2018.02.23 更新:
原计划是在春节期间也要把这个系列继续写下去的,然后刚好遇到了这一章,但可以断定,由于这一章是本书最为重要的一章,加上没有大屏、双屏的环境,硬看这部分真的太难了,看来一个高效的学习环境是多么重要(哈哈,这个有点像是一个借口)。这一篇讲了很多创建对象的模式,深入理解实例与原型间的关键是理解的核心,建议多搜索一些网上的教程辅助理解,包括廖雪峰老师的教程,从不同角度或者不同的例子对理解都很有好处。
今天大部分时间是在赶路中,晚上才继续来完成作业,不过本章的第二部分真是很多细节...需要比较大量的篇幅加上代码来解释,完全使用思维导图效果并不佳,目前只能把半成品发出来。明天会把相对详细的过程文字再梳理一遍,另外还在考虑中间的一些图如何处理会好一些...
创建对象
1.工厂模式
在 ECMAScript 中无法创建类,使用函数来封装以特定接口创建对象的细节。
function createPerson(name, age, job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
alert(this.name);
};
return o;
}
var person1 = createPerson("Nicklas", 29, "Software Engineer");
var person2 = createPerson("Gred", 27);
以上函数能够根据接收的参数来构建一个包含所有必要信息的 Person 对象。可以多次调用这个函数,每次返回一个包含三个属性一个方法的对象。工厂模式解决了创建多个相似对象的问题,但没解决对象识别问题。
2.构造函数模式
创建自定义的构造函数,从而自定义自定义对象类型的属性和方法,以上例子重写如下:
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
alert(this.name);
};
}
var person1 = new Person("Nicklas", 29, "Software Engineer");
var person2 = new Person("Gred", 27, "Doctor");
以上代码中
- 没有显式地创建对象;
- 直接将属性和方法赋给了
this
对象; - 没有
return
语句
person1
和person2
分别保存着 Person
的一个不同的实例,两个对象都有一个 constructor
(构造函数)属性,该属性指向 Person
。
这个例子中创建的所有对象既是 Object
的实例,同时也是 Person
的实例,使用 instanceof
可以得到验证。
person1 instanceof Object; // true
person1 instanceof Person; // true
2.1 将构造函数当做函数
任何函数只要通过 new
操作符来调用,都可以作为构造函数,否则为普通函数调用。
如果不写
new
,这就是一个普通函数,它返回undefined
。但是,如果写了new
,它就变成了一个构造函数,它绑定的this
指向新创建的对象,并默认返回this
,也就是说,不需要在最后写return this;
。
// 当做构造函数使用
var person = new Person("Nicklas", 29, "Engineer");
person.sayName(); // "Nicklas"
// 作为普通函数调用
Person("Greg", 27, "Doctor"); // 添加到window
window.sayName(); // "Greg"
// 在另一个对象的作用于中调用
var o = new Object();
Person.call(o, "Kisten", 25, "Nurse");
o.sayName(); // "Kisten"
2.2 构造函数的问题
每个方法都要在每个实例上重新创建一遍,以上代码中每个实例都有一个 sayName()
方法。在 ECMAScript 中,每个函数都是对象,每定义一个函数,都实例化了一个对象,也就是说不同实例上的同名函数是不相等的。
虽然可以把 sayName()
放在全局作用域下,但相应的引用类型就失去了封装性。不过好在这些问题可以通过原型模式来解决。
3 原型模式
创建的每个函数都有一个 prototype
(原型)属性(一个指针),指向一个对象,这个对象可包含特定类型的所有实例共享的属性和方法。也就是说 prototype
就是对象实例的原型对象,可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而可以将这些信息直接添加到原型对象中。
function Person() {
}
Person.prototype.name = "Nicklas";
Person.prototype.age = 29;
Person.prototype.job = "Engineer";
Person.prototype.sayName = function() {
alert(this.name);
}
var person1 = new Person();
person1.sayName(); // "Nicklas"
var person2 = new Person();
person2.sayName(); // "Nicklas"
person1.sayName == person2.sayName // true
以上代码将属性和方法添加到 Person
的 prototype
属性中,构造函数成了空函数,但通过构造函数创建的新对象都具有相同的属性和方法,这些属性和方法都是所有实例共享的。
3.1 理解原型对象
任何时候只要创建一个新函数,就会根据一组特定规则为该函数创建一个 prototype
属性,这个属性指向函数的原型对象。默认情况下,所有原型对象都会自动获得一个 constructor
(构造函数)属性,这个属性包含一个指向 prototype
属性所在函数的指针。
创建自定义的构造函数后,其原型对象默认只会取得 constructor
属性,其他方法则从 Object
继承而来。调用构造函数创建一个新实例后,实例内部将包含一个指针(内部属性),指向构造函数的原型对象,ES 5 中称其为 [[Prototype]]
,脚本中无标准方式访问,浏览器通过 __proto__
实现。这个连接存在于实例与构造函数的原型对象之间,不是存在于实例与构造函数之间。
使用之前 Person
构造函数和 Person.prototype
创建实例的代码为例说明上图各个对象之间的关系。
上图中有 Person
构造函数、 Person
的原型属性以及 Person
的两个实例。
-
Person.prototype
指向了原型对象; -
Person.prototype.constructor
又指回了Person
; - 原型对象中除了包含
constructor
属性之外,还包含后来添加的其他属性; -
Person
的每个实例person1
、person2
都包含一个内部属性([[Prototype]]
或__proto__
)指向Person.prototype
。
在所有实现中无法访问到 [[Prototype]]
,但可通过 isPrototypeOf()
方法,但可以通过 isPrototype()
方法来确定对象之间是否存在这种关系,即 [[Prototype]]
指向调用 isPrototype()
方法的对象(Person.prototype
),那么该方法返回 true
。
Person.prototype.isPrototypeOf(person1); // true
Person.prototype.isPrototypeOf(person2); // true
ES 5 中增加了一个新方法 Object.getPrototypeOf()
,返回 [[Prototype]]
的值,也就是对象的原型。
Object.getPrototypeOf(person1) == Person.prototype; // true
Object.getPrototypeOf(person1).name; // "Nicklas"
虽然可以通过对象实例访问保存在原型中的值,但不能通过对象实例重新原型中的值,若在实例中添加一个与实例原型中同名的属性,则该实例属性将会屏蔽原型中的同名属性。将这个同名的实例属性设置为 null
时,修改的也仅仅是该实例属性,也就是说不会恢复指向原型的连接,是用 delete
操作符则可以完全删除实例属性,恢复原型属性的访问。
使用 hasOwnProperty()
方法(从 Object
中继承而来)可以检测一个属性是否存在于实例中还是原型中,当属性存在于对象实例中时返回 true
。类似的,ES 5 中的 Object.getOwnPropertyDescriptor()
方法只能用于实例属性,若要取得原型属性的描述符,必须直接在原型对象上调用 Object.getOwnPropertyDescriptor()
方法。
下面两张图来源于廖雪峰的 JavaScript 教程和知乎的一个回答,帮助理解原型对象。
3.2 原型与 in 操作符
两种方式使用 in
操作符:
-
for - in
循环中使用 - 单独使用
单独使用时,in
操作符在通过对象可访问给定属性时返回 true
,无论该属性存在于实例还是原型中。
"name" in person1; // true
构造如下函数判断该属性存在于实例还是原型中:
function hasPrototypeProperty(obj, name) {
return !obj.hasOwnProperty(name) && (name in obj);
}
hasPrototypeProperty(person1, "name"); // true
person1.name = "Greg";
hasPrototypeProperty(person1, "name"); // false
3.3 更简单的原型语法
为了避免每添加一个属性都要输入一遍 Person.prototype
,一般使用字面量来重写整个原型对象。
function Person() {
}
Person.prototype = {
name: "Nicklas",
age: 29,
job: "Engineer",
sayName: function () {
console.info(this.name);
}
}
以上方法会使 constructor
属性不再指向 Person
,因此通过 constructor
已经无法确定对象的类型。可以通过如下设置保持不变。
Person.prototype = {
constructor: Person,
name: "Nicklas",
age: 29,
job: "Engineer",
sayName: function () {
console.info(this.name);
}
}
3.4 原型的动态性
由于在原型中查找值的过程是一次搜索,因此即便是在创建实例后修改原型也能在实例中反映出相应的修改,如在原型中增加一个方法,再使用实例调用该方法也能正常执行。这是因为实例与原型之间的松散连接关系,即实例与原型之间的连接为指针而非副本。但如果重写原型则无法实现以上的情形,因为修改原型为另外的对象则切断了构造函数与最初原型之间的联系。
3.5 原生对象的原型
原生的引用类型也是使用原型模式创建,如在 Array.prototype
中可以找到 sort()
方法,在 String.prototype
中可以找到 substring()
方法。因此通过原生对象的原型,不仅可以取得所有默认方法的引用,也可以定义新方法。
3.6 原生对象的问题
首先,原型模式省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。
其次,由于其共享本性,原型中的所有属性是被很多实例共享,虽然可以通过在实例删添加同名属性隐藏原型中的属性,但对于包含引用类型的属性来说则问题比较突出,因为修改实例的引用类型属性同样会修改原型中的相应属性,毕竟是引用类型。
4 组合使用构造函数模式和原型模式
构造函数模式用于定义实力属性,而原型模式用于定义方法和共享的属性。重写例子如下:
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.friends = ["shely", "court"];
}
Person.prototype = {
constructor : Person,
sayName : function() {
alert(this.name);
}
}
5 动态原型模式
动态原型模式解决了构造函数和原型独立的问题,它将所有信息封装构造函数中,在通过构造函数中初始化原型,又保持了同时使用构造函数的原型的优点,也就是通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。
function Person(name, age, job) {
// 属性
this.name = name;
this.age = age;
this.job = job;
this.friends = ["shely", "court"];
// 方法
if (typeof this.sayName != "function") {
Person.prototype.sayName = function() {
alert(this.name);
}
}
}
6 寄生构造模式
基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。
7 稳妥构造函数模式
所谓稳妥对象是指没有公共属性,而且其方法也不引用 this
的对象,与寄生构造模式相区别,主要是:1、新创建对象的实例方法不引用 this
,2、不使用 new
操作符调用构造函数。
后记
说实话,写到这里,这个中篇终于结束了,从春节前写到了春节后,甚是艰难,这一节总共看了三遍,在回到办公环境双屏的情况下终于有效地学完了。惯例放一个思维导图,但是核心还是在文章中。