JS对象进阶-创建对象的5种模式
在JS对象类型-对象-认识对象一文中,介绍了创建对象的3种方式:new构造函数、对象字面量和Object.create()函数。下面基于这3种方式介绍下创建对象的5种常用模式。
对象字面量
var p1 = {
name: 'li',
age: 10,
sex: 'boy'
}
var p2 = {
name: 'wang',
age: 20,
sex: 'girl'
}
对象字面量方式虽然可以创建单个对象,但是如果同时创建多个类似对象,会产生大量冗余代码。
工厂模式
为了解决创建多个对象造成代码冗余的问题,人们发明了工厂模式。该模式通过函数来封装特定的接口。
function People(name,age,sex) {
var o = new Object();
o.name = name;
o.age = age;
o.sex = sex;
o.sayName = function(){
console.log(this.name);
}
return o;
}
var p1=People('li', 10, 'boy');
var p2=People('wang', 20, 'girl');
console.log(p1 instanceof People) // false
使用这种方式虽然解决了创建多个对象的问题,但是却没办法识实例对象的类型。
构造函数模式
通过创建自定义构造函数来自定义对象的属性和方法,这种方式可以把实例标识为一种特定类型,这也是构造函数胜过工厂模式的原因。
function People(name, age, sex) {
this.name=name;
this.age=age;
this.sex=sex;
this.sayName=function() {
console.log(this.name);
}
}
var p1=new People('li', 10, 'boy');
var p2=new People('wang', 20, 'girl');
console.log(p1 instanceof People); // true
看起来似乎已经完美了,其实使用构造函数模式也有一些问题。它会把每个方法在实例上重新创建一遍,创建多个完成同样任务的函数是没有必要的。
// 两个实例的sayName方法占用不同的内存空间
console.log(p1.sayName===p2.sayName); //false
构造函数拓展模式
为了解决构造函数重复创建相同方法的问题,在构造函数的基础上引入拓展模式,即把方法移到构造函数外部。
function People(name, age, sex) {
this.name=name;
this.age=age;
this.sex=sex;
}
function sayName{
console.log(this.name);
}
var p1=new People('li', 10, 'boy');
var p2=new People('wang', 20, 'girl');
console.log(p1.sayName===p2.sayName); //true
使用这种方式又有了新问题,把方法定义到全局作用域中显然污染了全局空间,如果方法有很多肯定会乱成一锅粥,更没有封装性可言了。
寄生构造函数模式
寄生构造函数模式是工厂模式和构造函数的结合,它兼具了这两种模式的缺点,没错是缺点。不仅每个方法都要在每个实例上重新创建一遍,而且没办法识实例对象的类型。所以尽量不要使用这种模式。
function People(name, age, sex) {
var o=new Object();
o.name=name;
o.age=age;
o.sex=sex;
o.sayName=function() {
console.log(this.name);
}
return o;
}
var p1=new People('li', 10, 'boy');
var p2=new People('wang', 20, 'girl');
console.log(p1.sayName === p2.sayName) // false
console.log(p1 instanceof People) // false
稳妥构造函数模式
稳妥对象没有公共属性,并且方法不能引用this对象。稳妥模式可以防止数据内其他应用程序修改,所以适合在一些安全环境中使用。
function People(name, age, sex) {
var o=new Object();
o.sayName=function() {
console.log(name);
}
return o;
}
var p1=People('li', 10, 'boy');
var p2=People('wang', 20, 'girl');
console.log(p1.sayName===p2.sayName) // false
console.log(p1 instanceof People) // false
和寄生模式类似,不仅每个方法都要在每个实例上重新创建一遍,而且没办法识实例对象的类型。
原型模式
通过原型对象,让所有实例共享它的属性和方法。
function People(name, age, sex) {
People.prototype.name = name;
People.prototype.age = age;
People.prototype.sex = sex;
People.prototype.sayName = function(){
console.log(this.name)
}
}
var p1=new People('li', 10, 'boy')
var p2=new People('wang', 20, 'girl')
console.log(p1.sayName===p2.sayName) // true
console.log(p1 instanceof People) // true
【更简单简洁的原型模式】
function People() {}
People.prototype = {
name: 'li',
age: 10,
sex: 'boy',
sayName: function(){
console.log(this.name)
}
};
var p1 = new People();
p1.sayName(); // 'li'
缺点: 这种简洁的方式没办法初始化参数。
注意: 使用包含所有属性和方法的对象字面量重写原型对象后,constructor属性不再指向People构造函数。因为重写后没有显式的定义constructor属性,它就只能从原型链上Object.prototype中获取constructor属性。
p1.constructor === People // false
p1.constructor === Object // true
解决方法是显式的设置constructor属性。
function People() {}
People.prototype= {
constructor: People,
name: 'li',
age: 10,
sex: 'boy',
sayName: function() {
console.log(this.name)
}
}
var p1 = new People();
p1.constructor===People; // true
另外原生的constructor属性是不可枚举的,所以为了保持一致,最好使用Object.defineProperty()方法把constructor属性设置成不可枚举的。
function People() {}
People.prototype= {
constructor: People,
name: 'li',
age: 10,
sex: 'boy',
sayName: function() {
console.log(this.name)
}
}
Object.defineProperty(People.prototype, 'constructor', {
enumerable: false,
value: People
})
var p1=new People();
p1.constructor===People; // true
使用原型模式也不是最完美的,它的问题是引用类型值的属性可以被所有实例共享和修改。
function People() {};
People.prototype.hobbies=['阅读','编程','跑步'];
var p1=new People()
var p2=new People()
p1.hobbies.push('音乐')
console.log(p2.hobbies); // ["阅读", "编程", "跑步", "音乐"]
组合模式
组合模式是构造函数模式和原型模式的结合,是创建自定义类型最常用的方式。构造函数模式用于定义实例的属性,原型模式用于定义方法和共享的属性。每个实例都有自己的属性副本,并且共享对方法的引用。组合模式是目前使用最广泛的创建自定义对象的模式。
function People(name,age,sex,hobbies) {
this.name = name
this.age = age
this.sex = sex
this.hobbies = hobbies
}
People.prototype = {
constructor: People,
school: 'Tsinghua',
sayName: function() {
console.log(this.name)
}
}
var p1=new People('li', 10, 'boy', ['音乐'])
var p2=new People('wang', 20, 'girl',['编程'])
p1.hobbies.push('跑步')
console.log(p2.hobbies) // ['编程']
p1.__proto__.school = 'Hafer'
console.log(p2.school) // 'Hafer'
console.log(p1.sayName === p2.sayName) // true
console.log(p1 instanceof People) // true
动态原型模式
动态原型模式把组合模式中的构造函数和原型对象都封装到了构造函数中,通过检查方法是否被创建来决定是否初始化原型对象。
function People(name,age,sex,hobbies) {
this.name = name
this.age = age
this.sex = sex
this.hobbies = hobbies
if(typeof this.sayName != 'function') {
console.log(typeof this.name)
People.prototype.sayName = function() {
console.log(this.name)
}
}
}
var p1=new People('li', 10, 'boy', ['音乐'])
console.log(p1.sayName())
总结
【字面量方式】
缺点:多个对象造成代码冗余
【工厂模式】
缺点:无法识别实例类型
【构造函数模式】
缺点:每个实例都会创建一个相同的方法,造成内存浪费
【原型模式】
缺点:引用类型值的属性会被所有实例共享和修改
【组合模式】
目前使用最广泛的一种模式,解决上了以上模式的所有缺点。