文中内容大部分整理自 《JavaScript 高级程序设计 (第三版)》,并辅以部分我个人的理解。如有遗漏或错误,欢迎斧正。

理解对象中的原型

  • 只要创建一个新函数,就会为函数创建一个 prototype 属性,指向原型对象,原型对象会获得一个 constructor 属性,指向 prototype 属性所在函数的指针。假如有一个函数 Person () {},那么 Person.prototype 指向原型对象,Person.prototype.constructor 又指回了 Person
  • 构造函数、原型和实例之间的关系: 每个构造函数都有一个原型对象 (Person.prototype),原型对象都包含一个指向构造函数的指针 (constructor),实例包含一个指向原型对象的内部指针 (__proto__)
  • 每个对象都有 __proto__,而只有函数才有 prototype
  • 原型链查找就是通过 __proto__ 查找,查找至值为 null (也就是 Object.prototype) 时结束
  • 每当我们通过 new Foo(x) 创建对象时,JavaScript 内部会首先创建一个新的对象,将其 __proto__ 属性指向 Foo 的原型,也就是 Foo.prototype,然后将构造函数中的 this 指向刚刚创建的新对象,最后再执行构造函数中的代码
  • 在构建原型链时,如果直接重写 prototype 属性,则相当于切断了原型与构造函数之间的联系,此时 prototype.constructor 将指向 Object 构造函数而非 Person 函数
  • 如果重写函数的 prototype,即使不重新指定 constructor 属性也可以使实例获取原型中的属性和方法,这两者的区别在于能否通过 __proto__ 找到实例所属的原型。见下例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function Person () {}
Person.prototype = {
name: 'Person',
sayName: function () { return this.name; }
}

function Animal () {}
Animal.prototype = {
constructor: Animal,
name: 'Animal',
sayName: function () { return this.name; }
}

var p = new Person();
var a = new Animal();

console.log(p.__proto__.constructor)
// f Object() { }
console.log(a.__proto__.constructor)
// f Person() { }

console.log(p.sayName())
// Person
console.log(a.sayName())
// Animal
  • 重写 prototype 将造成,已经被创建的对象将无法访问修改后的原型属性或方法。重新指定 prototype.constructor 为构造函数可以让实例的 __proto__ 重新指回 Animal 而不是 Object,但重新指定仍然不能恢复原型与构造函数之间的联系,已经被创建的对象仍将无法访问修改后的原型属性或方法。见下例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Person () {}
function Animal () {}

var p = new Person();
var a = new Animal();

Person.prototype.sayName = function () { return 'Person' };
Animal.prototype = {
// 加入这句话可以保证 constructor 重新指向 Animal 函数
constructor: Animal,
sayName: function () { return 'Animal' }
}

console.log(p.__proto__)
// Person { constructor: Person, sayName: f() }
console.log(a.__proto__)
// Person { constructor: Animal }

console.log(p.sayName())
// Person
// 由于实例和原型之间的连接是一个指针,因此 p 可以在原型中找到 sayName 属性
console.log(a.sayName())
// TypeError: a.sayName is not a function
// 原型与构造函数之间的联系已经被切断,a 引用的仍然是最初的原型

创建对象的几种方式

工厂模式

1
2
3
4
5
6
7
function Person (name, age) {
var o = new Object();
// 此处也可以是 new Array() 或 new Date() 等,只要能返回一个对象即符合该模式
o.name = name;
o.age = age;
return o;
}

缺点: 无法知道对象的类型

构造函数

1
2
3
4
5
6
7
8
9
function Person (name, age) {
this.name = name;
this.age = age;
this.sayName = function () {
return this.name;
};
}

var p = new Person('zy', 18);

缺点: 每次创建实例,构造函数上的方法都需要在实例上重新创建一遍,而非共用方法,浪费内存。

改进版:

1
2
3
4
5
6
7
8
9
10
11
function Person (name, age) {
this.name = name;
this.age = age;
this.sayHi = sayName;
}

function sayName () {
return this.name;
}

var p = new Person('zy', 18);

这种方式虽然实现了不同实例间可共用一个方法,但是每个方法都需要在全局环境定义,毫无封装性可言。

原型模式

1
2
3
4
5
6
7
8
function Person () {}

Person.prototype.name = 'zy';
Person.prototype.sayName = function () {
return this.name;
};

var p = new Person();

缺点: 原型属性中的引用类型会在实例间共享,某一个实例进行修改其他实例都会发生改变。

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

这种方式是使用最广,认同最高的方式。总结来讲是通过构造函数定义实例属性,通过原型定义共享的方法 (也可以定义共享的属性)。这样可以保证每个实例都会有实例属性的副本,但同时共享对方法的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person (name) {
this.name = name;
this.characteristics = ['speakable', 'legs'];
}

Person.prototype.sayName = function () { return this.name };

var p1 = new Person('Bob');
p1.characteristics.push('male');
var p2 = new Person('Violet');
p2.characteristics.push('female');

console.log(p1.characteristics) // ['speakable', 'legs', 'male']
console.log(p2.characteristics) // ['speakable', 'legs', 'female']
console.log(p1.sayName === p2.sayName) // true

动态原型模式

优点是可以把所有的属性和方法都封装在构造函数中。实现方法是通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。

1
2
3
4
5
6
7
8
9
10
function Person (name, age) {
this.name = name;
this.age = age;

// 只需要检查一个方法即可
if (typeof this.sayName !== 'function') {
Person.prototype.sayName = function () { return this.name; }
Person.prototype.sayAge = function () { return this.age; }
}
}

寄生构造函数模式

和工厂模式类似,可以在特殊的情况下用来为对象创建构造函数。一般不建议使用这种模式。

1
2
3
4
5
6
7
function SpecialArr () {
var values = new Array();
values.push.apply(values, arguments);
values.toPipedStr = function () { return this.join('|') };

return values;
}

稳妥构造函数模式

没有公共属性,也不使用 this。除了调用特定的方法外无法直接访问属性。与寄生构造函数类似,但有两点不同:

  • 新创建对象的实例方法不实用 this
  • 不实用 new 操作符调用构造函数
1
2
3
4
5
6
7
8
9
10
function Person () {
var o = new Object();
var name = 'zy';
var age = 18;

// 只能通过 sayName 方法获取 name 的指
o.sayName = function () { return name; }

return o;
}

继承

重写原型链实现的继承

  • 重写某个构造函数的原型对象,代之以一个新类型的实例。原来存在于父级实例中的属性和方法,也将存在于子级的原型对象中
  • 实例的 __proto__ 指向该对象的构造函数的原型对象
1
2
3
4
5
6
7
8
9
function SuperType() { this.property = true; }
SuperType.prototype.getSuperValue = function () { return this.property; };

function SubType() { this.subproperty = false; }
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () { return this.subproperty; };

var sub = new SubType();
console.log(sub.getSuperValue()); // true

原型链图示:

缺点:

  • SubType 的实例都会共享 SuperType 的引用类型属性
  • 创建子类型时不能向父类型的构造函数中传递参数

使用构造函数

在子类型构造函数的内部调用父类的构造函数。

1
2
3
4
5
6
7
8
9
10
11
function SuperType (name, age) { this.colors = ['red', 'blue', 'green'] }
function SubType (name, age) {
SuperType.call(this, name, age);
}

var s1 = new SubType('Bob', 18);
var s2 = new SubType('Mick', 20);
s2.colors.push('black');

console.log(s1.colors)
console.log(s2.colors)

缺点: 方法都只能在构造函数中定义,无法进行函数复用。而且父类型中定义的方法子类型中不可见。

组合继承

使用原型链实现对原型属性和方法的继承,通过构造函数实现对属性的继承。这样既通过在原型上定义方法实现函数的复用,又保证每个实例都有自己的属性。

缺点: 无论什么情况下,都会调用两次父类型的构造函数,一次是在创建子类型的时候,一次是在子类型的构造函数内部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function SuperType (name) {
this.name = name;
this.colors = ['red', 'blue'];
}
SuperType.prototype.sayName = function () { return this.name; }

function SubType (name, age) {
// 第二次调用父类型的构造函数,传参并覆盖
SuperType.call(this, name);
this.age = age;
}
// 第一次调用父类型的构造函数,初始化所有属性
// 此时 SubType.prototype 将得到 name 和 colors 两个来自父级的属性
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function () { return this.age; }

原型式继承

不使用构造函数,使用原型基于已有的对象创建新对象。要求必须有一个对象作为另一个对象的基础。

  • 在 ES5 中通过 Object.create(obj, [extraProps]) 方法规范化了原型式继承
  • 就像使用原型模式一样,引用类型的属性将会共享值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function createObj (o) {
function F() {}
F.prototype = o;
return new F();
}

var person = {
name: 'Bob',
friends: ['Van', 'Mike']
};

var p1 = createObj(person);
p1.name = 'Linda';
p1.friends.push('Aiden');
var p2 = createObj(person);

console.log(p1.name);
// Linda
console.log(p1.friends);
// ['Van', 'Mike', 'Aiden']
console.log(p2.friends);
// ['Van', 'Mike', 'Aiden']

寄生式继承

创建一个仅用于封装继承过程的函数,凡是能够返回新对象的函数都适用于此种模式。下例为 JS 高程中给出的例子:

1
2
3
4
5
6
7
8
9
function createAnother (original) {
// 并不一定需要 object(original),只要能返回新对象都可以
var clone = object(original);
clone.sayHi = function () { return 'hi'; }
return clone;
}

var person = { name: 'Bob' };
var anotherPerson = createAnother(person);

寄生组合式继承

这种方式是基于类型继承的最有效方式。

实现方式是通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function inheritPrototype(subType, superType){
var prototype = Object(superType.prototype); //创建对象
prototype.constructor = subType; //增强对象
subType.prototype = prototype; //指定对象
}

function SuperType (name) {
this.name = name;
this.colors = ['red', 'blue'];
}
SuperType.prototype.sayName = function () { return this.name; }

function SubType (name, age) {
// 此处使用 call() 改变了 this 指向,使 SuperType 原型中的方法都能在当前作用域使用
SuperType.call(this, name);
this.age = age;
}

inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function () { return this.age; }

ES6 中的类与继承

在 ES6 中引入了 Class 这个概念,通过 class 关键字定义类,并且可以通过 extends 关键字实现继承。但是 class 只是语法糖,在 JS 内部仍然是使用原型链实现继承,因此能够使用 Class 实现的功能一定可以通过原型实现。

ES6 语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Person {
constructor (name, age) {
this.name = name;
this.age = age;
}

static move () {
return 'Use legs';
}

sayInfo () {
return `Name: ${this.name}, Age: ${this.age}`;
}
}

class Student extends Person {
constructor (name, age, school) {
super(name, age);
this.school = school;
}

study () {
return `I am studying at ${this.school}.`;
}
}

对应到 ES5 语法如下 (使用了组合继承方式):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 父类及构造函数
function Person (name, age) {
this.name = name;
this.age = age;
}
// 静态方法
Person.move = function () { return 'Use legs'; }
// 属于父类的方法
Person.prototype.sayInfo = function () {
return `Name: ${this.name}, Age: ${this.age}`;
}

// 子类
function Student (name, age, school) {
Person.call(this, name, age);
this.school = school;
}
Student.prototype = new Person();
Student.prototype.constructor = Student;
Student.prototype.study = function () {
return `I am studying at ${this.school}.`;
}

参考资料