前言
1 | // 字面量方式创建对象 |
虽然Object构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码,如上面的代码,每创建一个类似的person对象,就会重复上面的写法,代码较为冗余。
工厂模式
1 | function createPerson(name, age, job) { |
工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
构造函数模式
使用构造函数模式将前面的例子重写如下:
1 | function Person(name, age, job) { |
要创建 Person 的新实例,必须使用 new 操作符。以这种方式调用构造函数实际上会经历以下 4个步骤:
- 创建一个新对象
- 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象)
- 执行构造函数中的代码(为这个新对象添加属性)
- 返回新对象(构造函数在不返回值的情况下,默认会返回新对象实例)
在前面例子的最后,person1 和 person2 分别保存着 Person 的一个不同的实例。这两个对象都有一个constructor(构造函数)
属性,该属性指向 Person,如下所示。
1 | console.log(person1.constructor == Person); //true |
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方。在这个例子中,person1 和 person2 之所以同时是 Object 的实例,是因为所有对象均继承自 Object。
使用构造函数的主要问题,就是每个方法(这里是sayname)都要在每个实例上重新创建一遍。
1 | console.log(person1.sayName == person2.sayName) // false |
原型模式
1 | function Person() {} |
与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的。换句话说,person1 和 person2 访问的都是同一组属性和同一个 sayName()函数。
更简单的原型语法
1 | function Person() {} |
在上面的代码中,我们将 Person.prototype 设置为等于一个以对象字面量形式创建的新对象。最终结果相同,但有一个例外:constructor 属性不再指向 Person 了。前面曾经介绍过,每创建一个函数,就会同时创建它的prototype 对象,这个对象也会自动获得 constructor 属性。而我们在这里使用的语法,本质上完全重写了默认的 prototype 对象,因此 constructor 属性也就变成了新对象的 constructor 属性(指向 Object 构造函数),不再指向 Person 函数。此时,尽管 instanceof 操作符还能返回正确的结果,但通过 constructor 已经无法确定对象的类型了。
如果 constructor 的值真的很重要,可以像下面这样特意将它设置回适当的值。
1 | function Person() {} |
调用构造函数时会为实例添加一个指向最初原型的 [[Prototype]]
指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。
请记住:实例中的指针仅指向原型,而不指向构造函数。
1 | function Person() {} |
在上面的代码中,我们先创建了 Person 的一个实例,然后又重写了其原型对象。然后在调用 friend.sayName()时发生了错误,因为 friend 指向的原型中不包含以该名字命名的属性。
原型对象的问题
原型中所有属性是被很多实例共享的,这种共享对于函数非常合适。对于包含引用类型值的属性来说,问题就比较突出了。而这个问题正是我们很少看到有人单独使用原型模式的原因所在。
1 | function Person() {} |
组合使用构造函数模式和原型模式
1 | function Person(name, age, job) { |
在这个例子中,实例属性都是在构造函数中定义的,而由所有实例共享的属性 constructor 和方法 sayName()则是在原型中定义的。而修改了 person1.friends(向其中添加一个新字符串),并不会影响到 person2.friends,因为它们分别引用了不同的数组。
这种构造函数与原型混成的模式,是目前在 ECMAScript 中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。