要点

● 在 ES6 之前,类以及类的继承机制没有明确定义,通过模仿来实现,以下内容针对 ES6 之前场景。
● 从 ES6 开始,类和继承有了定义,分别通过 class 和 extends 实现。
● Babel 对于 class 和 extends 向 ES6 之前的转译,使用的就是以下内容中“寄生组合式继承”的方式。

对象冒充(object masquerading)

■”对象冒充”也称为”借用构造函数”,通过以子类构造函数的上下文执行父类构造函数来实现,例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function ClassA(color) {
this.color = color;
this.sayColor = function () {
alert(this.color);
};
}
function ClassB(color, name) {
ClassA.call(this, color);

this.name = name;
this.sayName = function () {
alert(this.name);
};
}

■ 缺点:
(1)只能继承父类构造函数中的定义。
(2)父类构造函数中定义的方法在每次子类实例化的时候也会被创建一遍。

原型链(prototype chaining)

■ 原型链方式通过将父类实例赋值给子类构造函数的 prototype 实现。
■ 实例:

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 ClassA(color) {
this.color = color;
this.colorArr = [color];
}
ClassA.prototype.sayColor = function () {
alert(this.color);
};

function ClassB() {
this.name = "";
}
ClassB.prototype = new ClassA('white'); // 所有ClassB实例的__proto__都指向这个ClassA的实例
ClassB.prototype.constructor = ClassB; // 手动指定constructor属性值
ClassB.prototype.sayName = function () {
alert(this.name);
};

var objOne = new ClassB();
var objTwo = new ClassB();
objOne.color = 'red'; // 将创建objOne自己的‘color’属性而不会改变原型链中的color
objOne.colorArr.push('red');

console.log(`color: ${objOne.color}, colorArr: ${objOne.colorArr}`);// 打印 color: red, colorArr: white,red
console.log(`color: ${objTwo.color}, colorArr: ${objTwo.colorArr}`);// 打印 color: white, colorArr: white,red

■ 缺点:
(1)在子类实例化的时候无法给父类构造函数传递参数。
(2)由于所有子类实例的[[Prototype]]共享同一个父类实例,改写父类中引用类型的成员变量将造成相互污染(非引用类型变量没有这个问题,因为会在子类实例上创建新属性)。

组合继承(对象冒充 + 原型链)

■ 组合继承是对象冒充方法与原型链方法的结合,融合了两种方法的优点:
(1)通过原型链继承原型的属性和方法,实现方法与共享属性的复用。
(2)通过对象冒充继承实例的属性,保证每个实例都有自己的属性。
■ 例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function ClassA(color) {
this.color = color;
this.colorArr = [color];
}
ClassA.prototype.sayColor = function () {
alert(this.color);
};

function ClassB(color, name) {
ClassA.call(this, color);
this.name = name;
}
ClassB.prototype = new ClassA();
ClassB.prototype.constructor = ClassB;
ClassB.prototype.sayName = function () {
alert(this.name);
};

■ 一点瑕疵:父类的构造函数被调用了两次。但不能直接”ClassB.prototype = ClassA.prototype”,因为会导致子类修改 prototype 也修改了父类的 prototype;
■ 解决构造函数被调用两次的问题之前先了解“原型式继承”与“寄生式继承”。
■ 原型式继承和寄生继承由道格拉斯·克罗克福德(Douglas Crockford,也是 JSON 的提出人)于 2006 年左右提出和推广。
■ 原型式继承和寄生继承没有使用严格意义上的构造函数,也没有创建子类,而是借助原型链基于已有的一个父类实例来直接创建子类实例,提供了一种有别于传统 new 方式创建子类实例的思路。

原型式继承

■ 原型式继承与原型链继承一样利用的是设置子类原型链,区别是没有显式创建子类而是直接创建子类实例并设置子类实例的[[Prototype]]为父类实例,具体体现在如下的 createObj 函数。
■ 不关心父类实例是如何被创建的,通过直接将父类实例放在子类实例的原型链上的方式达到父类也处于子类实例的原型链上的目的,例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function ClassA(color) {
this.color = color;
this.colorArr = [color];
}
ClassA.prototype.sayColor = function () {
alert(this.color);
};
var oneObjA = new ClassA('white');

function createObj(o) { //此过程被ES5新增的Object.create()方法实现
function F(){}
F.prototype = o; // 相当于原型链继承中的“ClassB.prototype = new ClassA()”
return new F();
}

var oneObjB = createObj(oneObjA);
var anotherObjB = Object.create(oneObjA);

console.log(oneObjB instanceof ClassA);// true
console.log(anotherObjB instanceof ClassA);// true

oneObjB.colorArr.push('red');
console.log(anotherObjB.colorArr); // ["white", "red"],原型链上的共享属性

寄生式(parasitic)继承

■ 寄生式继承是对原型式继承的进一步封装与扩展。思路是实现一个函数,在函数内通过原型式继承创建新对象,并对这个新对象进行增强(增加自定义属性),最后返回这个新对象,实例:

1
2
3
4
5
6
7
function createObj2 (o) {
var clone = createObj(o); // 创建新对象继承父类
clone.sayName = function () { // 增强新对象
console.log('hi');
}
return clone; // 返回这个新对象
}

寄生组合式继承(对象冒充 + 寄生)

■ 将父类的 prototype 作为被继承实例,通过寄生式继承实现子类 prototype 仅对父类 prototype 的继承,解决组合继承方式构造函数被调用两次的问题,例:

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
26
27
function ClassA(color) {
this.color = color;
this.colorArr = [color];
}
ClassA.prototype.sayColor = function () {
alert(this.color);
};

function ClassB(color, name) {
ClassA.call(this, color);
this.name = name;
}

inheritClass(ClassB, ClassA);

function inheritClass(subType, superType) {
var onePrototype = Object.create(superType.prototype); // 创建新对象继承父类
onePrototype.constructor = subType; // 增强新对象
subType.prototype = onePrototype;// 指定新对象
};
ClassB.prototype.sayName = function () {
alert(this.name);
};

var oneObjB = new ClassB('red', 'rose');
oneObjB.sayName();
oneObjB.sayColor();