JavaScript中创建对象的7种模式

工厂模式

用函数来封装以特定接口创建对象的细节,解决了创建大量相似对象的问题。

function createPerson(name, age) {
	var o = new Object();
	o.name = name;
	o.age = age;
	o.sayName = function() {
		alert(this.name);
	};
	return o;
}
var person1 = createPerson("Tom", 12);
var person2 = createPerson("Jerry", 10);

缺点:不能够识别到对象的类型。

构造函数模式

function Person(name, age) {
	this.name = name;
	this.age = age;
	this.sayName = function() {
		alert(this.name);
	};
}
var person1 = new Person("Tom", 12);
var person2 = new Person("Jerry", 10);

其中 new 操作符创建一个新实例时经历的步骤参考:new操作符的工作原理
使用 instanceof 操作符可以检测对象的类型。

alert(person1 instanceof Person);	//true

缺点:每个方法都要在每个实例上重复创建一遍,不能实现复用。

原型模式

创建一个构造函数,就会给这个构造函数添加一个 prototype 属性,指向构造函数的原型对象。所有的原型对象上,都会自动获得一个 constructor 属性,指向原型对象的构造函数。

创建一个构造函数后,其原型对象上默认只有一个 constructor 属性,其他的属性和方法都是从 Object 继承过来的。

当使用构造函数创建一个新的实例时,这个实例上会包含一个 [[prototype]] 属性,指向构造函数的原型对象。

function Person() {}
Person.prototype.name = "Tom";
Person.prototype.age = 12;
Person.prototype.sayName = function() {
	alert(this.name);
};
var person1 = new Person();
var person2 = new Person();

上面代码中对象之间的关系如下:

image-20211128201511527

原型模式可以让每个实例对象都共享原型对象的属性和方法。
isPrototypeOf() 方法可以用来确定实例对象和构造函数之间的关系。

alert(Person.prototype.isPrototypeOf(person1));		//true

Object.getPrototypeOf() 方法可以返回实例对象 [[Prototype]] 的值。

alert(Object.getPrototypeOf(person1) == Person.prototype);	//true
alert(Object.getPrototypeOf(person1).name);				//"Tom"

hasOwnProperty() 方法可以检测一个属性是存在于实例中还是存在于原型中。只有当这个属性存在于实例对象中才会返回 true。

alert(person1.hasOwnProperty("name"));		//false

单独使用 in 操作符可以检测到对象是否存在给定的属性,无论是在实例中还是在原型中。

alert("name" in person1);		//true

遍历对象的所有属性的方法

  • for...in 循环可以获得实例对象和原型对象的可枚举的属性。
  • Object.keys() 返回一个所有元素为字符串的数组,其元素来自于从给定的object上面可直接枚举的属性。
  • Object.getOwnPropertyNames() 返回指定对象上所有自身属性的属性名(包括不可枚举属性但不包括 Symbol 值作为名称的属性)组成的数组。

原型模式更简单的语法:

function Person() {}
Person.prototype = {
	name: "Tom",
	age: 12,
	sayName: function() {
		alert(this.name);
	}
}

上面的代码将 Person 的原型设置为了一个以字面量形式创建的新对象。此时,Person 的原型对象的 constructor 属性不再指向 Person 函数了。因此,需要将 constructor 属性设置指定值。

function Person() {}
Person.prototype = {
	constructor: Person,
	name: "Tom",
	age: 12,
	sayName: function() {
		alert(this.name);
	}
}

原型模式的缺点:当原型对象中属性的值为引用类型时,会出现问题。

function Person() {}
Person.prototype = {
	constructor: Person,
	name: "Tom",
	age: 12,
	friends: ["Dog", "Duck"],
	sayName: function() {
		alert(this.name);
	}
}
var person1 = new Person();
var person2 = new Person();
person1.friends.push("Tiger");
alert(person1.friends);		//"Dog,Duck,Tiger"
alert(person2.friends);		//"Dog,Duck,Tiger"

因为原型对象的属性是共享的,当 person1 修改 friends 数组时,其他实例对象的 friends 数组也会跟着改变。

组合使用构造函数模式和原型模式

构造函数模式定义实例属性,原型模式定义方法和共享的属性。

function Person(name, age) {
	this.name = name;
	this.age = age;
	this.friends = ["Dog", "Duck"];
}
Person.prototype = {
	constructor: Person,
	sayName: function() {
		alert(this.name);
	}
}
var person1 = new Person("Tom", 12);
var person2 = new Person("Jerry", 10);
person1.friends.push("Tiger");
alert(person1.friends);		//"Dog,Duck,Tiger"
alert(person2.friends);		//"Dog,Duck"

动态原型模式

function Person(name, age) {
	this.name = name;
	this.age = age;
	if(typeof this.sayName != "function") {
		Person.prototype.sayName = function() {
			alert(this.name);
		}
	}
}
var person = new Person("Tom", 12);
person.sayName();

把所有信息都封装在构造函数中,if 语句内的代码只在首次调用构造函数时才会执行。
在使用动态原型模式时,不能使用对象字面量重写原型。

寄生构造函数模式

function Person(name, age) {
	var o = new Object();
	o.name = name;
	o.age = age;
	o.sayName = function() {
		alert(this.name);
	};
	return o;
}
var person = new Person("Tom", 12);
person.sayName();

除了使用 new 操作符以外,和工厂模式是一样的。不能使用 instanceof 操作符来确定对象类型。

稳妥构造函数模式

function Person(name, age) {
	var o = new Object();
	o.name = name;
	o.age = age;
	o.sayName = function() {
		alert(name);
	};
	return o;
}
var person = Person("Tom", 12);
person.sayName();

和寄生构造函数模式差不多,两点不同:

  1. 新创建对象的实例方法不引用 this
  2. 不使用 new 操作符调用构造函数