JavaScript中创建对象的五种模式详解
本文深入解析JavaScript中创建对象的五种核心模式。从基础的Object实例化出发,详细对比工厂模式、构造函数模式、原型模式以及结合构造函数与原型的混合模式。重点探讨各模式在内存共享、实例识别及封装性上的差异,并介绍原型动态模式的实现原理,帮助开发者根据场景选择最优方案。
JavaScript对象创建概述
近期在阅读《javascript高级程序设计》(第二版)。
探讨javascript中对象的创建机制。
涉及工厂模式。
涉及构造函数模式。
涉及原型模式。
涉及结合构造函数和原型模式。
涉及原型动态模式。
引用类型与对象创建基础
面向对象语言通常具备类的概念,允许通过类创建具有相同属性和方法的多个对象实例。尽管从技术层面看,javascript是一门面向对象语言,但它并不具备传统意义上的类概念,所有事物均为对象。任何对象都是某种引用类型的实例,由已有的引用类型创建而来;这些引用类型既可以是语言内置的,也可以是自定义的。内置的引用类型包括:Object、Array、Data、RegExp、Function。引用类型本质上是一种数据结构,它将数据与功能组织在一起,通常被称作类。在缺乏类概念的javascript中,核心挑战在于如何高效地创建对象。
一般对象创建方法
1.1.0.创建对象的一般方法
var person = {}; //对象字面量表示,等同于var person = new Objcect();
person.name = 'evansdiy';
person.age = '22';
person.friends = ['ajiao','tiantian','pangzi'];
person.logName = function() {
console.log(this.name);
}
基于Object引用类型,可以创建一个包含四个属性的对象,其中一个是方法。如果需要创建大量类似person的实例,将会产生大量重复代码。
工厂模式
1.1.1.工厂模式 [top]
通过一个封装了对象细节的函数来创建对象,并在函数内部返回该对象。
function person(name,age,friends) {
var o = {
name: name,
age: age,
friends: friends,
logName: function() {
console.log(this.name);
}
};
return o;
}
var person1 = person('Evansdiy','22',['ajiao','tiantian','pangzi']);
每次调用person函数时,都会通过函数内部的对象o创建新对象并返回。除了创建新对象外,这个内部对象o没有其他用途。此外,无法判断工厂模式创建的对象的类型。
构造函数模式
1.1.2.构造函数模式 [top]
function Person(name,age,job) {
this.name = name;
this.age = age;
this.job = job;
this.logName = function() {
console.log(this.name);
}
}
//通过new操作符创建Person的实例
var person1 = new Person('boy-a','22','worker');
var person2 = new Person('girl-b','23','teacher');
person1.logName(); //boy-a
person2.logName(); //girl-a
与工厂模式相比,构造函数模式无需创建中间对象,也不使用return语句。此外,可以将构造函数的实例标识为特定类型,从而解决对象识别问题(通过检查实例的constructor属性,或利用instanceof操作符判断实例是否由某个构造函数创建)。
console.log(person1.constructor == Person);//constructor位于构造函数原型中,并指向构造函数,结果为true
console.log(person1 instanceof Person);//通过instanceof操作符,判断person1是否为构造函数Person的实例但构造函数模式也存在自身问题。实际上,logName方法在每个实例上都会被重新创建一次。需要注意的是,通过实例化创建的方法并不相等,以下代码将返回false:
console.log(person1.logName == person2.logName);//false我们可以将方法移到构造器外部(变为全局函数)来解决这个问题:
function logName() {
console.log(this.name);
}
function logAge() {
console.log(this.age);
}
然而,在全局下创建的全局函数实际上只能被经由Person创建的实例调用,这略显名不副实;如果方法众多,还需逐一定义,缺乏封装性。
原型模式
1.1.3.原型模式 [top]
javascript中的每个函数都包含一个指向prototype属性的指针(大部分浏览器可通过内部属性__proto__访问)。prototype属性是一个对象,其中包含了由某种引用类型创建的所有实例共享的属性和方法。
function Person() {}
Person.name = 'evansdiy';
Person.prototype.friends = ['ajiao','jianjian','pangzi'];
Person.prototype.logName = function() {
console.log(this.name);
}
var person1 = new Person();
person1.logName();//'evansdiy'
上述代码执行了以下操作:
1.定义了一个构造函数Person,Person函数自动获得一个prototype属性,该属性默认只包含一个指向Person的constructor属性;
2.通过Person.prototype添加三个属性,其中一个为方法;
3.创建一个Person的实例,随后在实例上调用了logName()方法。
这里需要特别注意logName()方法的调用过程:
1.在person1实例上查找logName()方法,发现没有这个方法,于是追溯到person1的原型
2.在person1的原型上查找logame()方法,有这个方法,于是调用该方法 基于这一查找过程,我们可以在实例上定义原型中的同名属性,以阻止该实例访问原型上的同名属性。需要注意的是,这样做并不会删除原型上的同名属性,仅仅是阻止实例访问。
var person2 = new Person();
person2.name = 'laocai';如果我们不再需要实例上的属性时,可以通过delete操作符删除。
delete person2.name;利用for-in循环枚举出实例可以访问到的所有属性(不论该属性存在于实例或是原型中):
for(i in person1) {
console.log(i);
}
同时,也可以利用hasOwnProperty()方法判断某个属性到底存在于实例上,还是存在于原型中,只有当属性存在于实例中,才会返回true:
console.log(person1.hasOwnProperty('name'));//true!hasOwnProperty来自Object的原型,是javascript中唯一一个在处理属性时不查找原型链的方法。[via javascript秘密花园] 另外,也可以通过同时使用in操作符和hasOwnProperty()方法来判断某个属性存在于实例中还是存在于原型中:
console.log(('friends' in person1) && !person1.hasOwnProperty('friends'));先判断person1是否可以访问到friends属性,如果可以,再判断这个属性是否存在于实例当中(注意前面的!),如果不存在于实例中,就说明这个属性存在于原型中。 前面提到,原型也是对象,所以我们可以用对象字面量表示法书写原型,之前为原型添加代码的写法可以修改为:
Person.prototype = {
name: 'evansdiy',
friends: ['ajiao','jianjian','pangzi'],
logName: function() {
console.log(this.name);
}
}
由于对象字面量语法重写了整个prototype原型,原先创建构造函数时默认取得的constructor属性会指向Object构造函数:
//对象字面量重写原型之后
console.log(person1.constructor);//Object不过,instanceof操作符仍会返回希望的结果:
//对象字面量重写原型之后
console.log(person1 instanceof Person);//true当然,可以在原型中手动设置constructor的值来解决这个问题。
Person.prototype = {
constructor: Person,
......
}
如果在创建对象实例之后修改原型对象,那么对原型的修改会立即在所有对象实例中反映出来:
function Person() {};
var person1 = new Person();
Person.prototype.name = 'evansdiy';
console.log(person1.name);//'evansdiy'
实例和原型之间的连接仅仅是一个指针,而不是一个原型的拷贝,在原型实际上是一次搜索过程,对原型对象的所做的任何修改都会在所有对象实例中反映出来,就算在创建实例之后修改原型,也是如此。 如果在创建对象实例之后重写原型对象,情况又会如何?
function Person() {};
var person1 = new Person1();//创建的实例引用的是最初的原型
//重写了原型
Person.prototype = {
friends: ['ajiao','jianjian','pangzi']
}
var person2 = new Person();//这个实例引用新的原型
console.log(person2.friends);
console.log(person1.friends);
上述代码在执行到最后一行时会出现未定义错误,如果我们用for-in循环枚举person1中的可访问属性时,会发现,里头空无一物,但是person2却可以访问到原型上的friends属性。 !重写原型切断了现有原型与之前创建的所有对象实例的联系,之前创建的对象实例的原型还在,只不过是旧的。
//创建person1时,原型对象还未被重写,因此,原型对象中的constructor还是默认的Person()
console.log(person1.constructor);//Person()
//但是person2的constructor指向Object()
console.log(person2.constructor);//Object()需要注意的是,原型模式忽略了为构造函数传递参数的过程,所有的实例都取得相同的属性值。同时,原型模式还存在着一个很大的问题,就是原型对象中的引用类型值会被所有实例共享,对引用类型值的修改,也会反映到所有对象实例当中。
function Person() {};
Person.prototype = {
friends: ['ajiao','tiantian','pangzi']
}
var person1 = new Person();
var person2 = new Person();
person1.friends.push('laocai');
console.log(person2.friends);//['ajiao','tiantian','pangzi','laocai']
修改person1的引用类型值friends,意味着person2中的friends也会发生变化,实际上,原型中保存的friends实际上只是一个指向堆中friends值的指针(这个指针的长度是固定的,保存在栈中),实例通过原型访问引用类型值时,也是按指针访问,而不是访问各自实例上的副本(这样的副本并不存在)。
混合模式与原型动态模式
1.1.4.结合构造函数和原型模式创建对象 [top]
结合构造函数和原型模式的优点,弥补各自的不足,利用构造函数传递初始化参数,在其中定义实例属性,利用原型定义公用方法和公共属性,该模式应用最为广泛。
function Person(name,age) {
this.name = name;
this.age = age;
this.friends = ['ajiao','jianjian','pangzi'];
}
Person.prototype = {
constructor: Person,
logName: function() {
console.log(this.name);
}
}
var person1 = new Person('evansdiy','22');
var person2 = new Person('amy','21');
person1.logName();//'evansdiy'
person1.friends.push('haixao');
console.log(person2.friends.length);//3
1.1.5.原型动态模式 [top]
原型动态模式将需要的所有信息都封装到构造函数中,通过if语句判断原型中的某个属性是否存在,若不存在(在第一次调用这个构造函数的时候),执行if语句内部的原型初始化代码。
function Person(name,age) {
this.name = name;
this.age = age;
if(typeof this.logName != 'function') {
Person.prototype.logName = function() {
console.log(this.name);
};
Person.prototype.logAge = function() {
console.log(this.age);
};
};
}
var person1 = new Person('evansdiy','22');//初次调用构造函数,此时修改了原型
var person2 = new Person('amy','21');//此时logName()方法已经存在,不会再修改原型
需要注意的是,该模式不能使用对象字面量语法书写原型对象(这样会重写原型对象)。若重写原型,那么通过构造函数创建的第一实例可以访问的原型对象不会包含if语句中的原型对象属性。
function Person(name,age) {
this.name = name;
this.age = age;
if(typeof this.logName != 'function') {
Person.prototype = {
logName: function() {
console.log(this.name);
},
logAge: function() {
console.log(this.Age);
}
}
};
}
var person1 = new Person('evansdiy','22');
var person2 = new Person('amy','21');
person2.logName();//'amy'
person1.logName();//logName()方法不存在
需要说明的是,各模式都有自己的应用场景,无所谓优劣。
以上这篇浅析在javascript中创建对象的各种模式就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持本站。
JavaScript闭包与类(Class)的区别及适用场景对比
2026-11-05 15:48:57
JavaScript入门要学ES6吗?let与箭头函数等新特性解析
2026-11-05 14:49:16
JavaScript 8种数据类型详解:基本类型与引用类型的区别
2026-11-05 14:25:15
怎么在JavaScript中使用async/await优化Ajax异步流程
2026-11-05 13:56:15
JavaScript replace回调函数实现动态正则替换逻辑
2026-11-05 13:52:13
