十二、JavaScript之继承的多种方式和优缺点

本文讲解 JavaScript 各种继承方式和优缺点。

原型链继承

实现方式:将子类的原型链指向父类的对象实例

1
2
3
4
5
6
7
8
9
10
11
12
function Parent() {
this.name = "parent";
this.list = ["a"];
}
Parent.prototype.sayHi = function () {
console.log("hi");
};
function Child() {}
Child.prototype = new Parent();
var child = new Child();
console.log(child.name); // parent
child.sayHi(); // hi

原理:子类实例 child 的__proto__指向 Child 的原型链 prototype,而 Child.prototype 指向 Parent 类的对象实例,该父类对象实例的__proto__指向 Parent.prototype,所以 Child 可继承 Parent 的构造函数属性、方法和原型链属性、方法。

1
2
child.__proto__ === Child.prototype; // true
child.__proto__.__proto__ === Parent.prototype; // true

优点:可继承构造函数的属性,父类构造函数的属性,父类原型的属性
缺点:无法向父类构造函数传参;且所有实例共享父类实例的属性,若父类共有属性为引用类型,一个子类实例更改父类构造函数共有属性时会导致继承的共有属性发生变化;实例如下:

1
2
3
4
var child2 = new Child();
console.log(child2.list); // ['a']
child.list.push("b");
console.log(child2.list); // ['a', 'b']

构造函数(经典继承)

实现方式:在子类构造函数中使用 call 或者 apply 劫持父类构造函数方法,并传入参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Parent(name, id) {
this.id = id;
this.name = name;
this.printName = function () {
console.log(this.name);
};
}
Parent.prototype.sayName = function () {
console.log(this.name);
};
function Child(name, id) {
Parent.call(this, name, id);
// Parent.apply(this, arguments);
}
var child = new Child("jin", "1");
child.printName(); // jin
child.sayName(); // Error

原理:使用 call 或者 apply 更改子类函数的作用域,使 this 执行父类构造函数,子类因此可以继承父类共有属性
优点:可解决原型链继承的缺点
缺点:不可继承父类的原型链方法,构造函数不可复用

组合继承

原理:综合使用构造函数继承和原型链继承。
其背后的思路是使用原型链实现对原型对象的属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。

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 Parent(name, id) {
this.id = id;
this.name = name;
this.list = ["a"];
this.printName = function () {
console.log(this.name);
};
}
Parent.prototype.sayName = function () {
console.log(this.name);
};
function Child(name, id) {
Parent.call(this, name, id);
// Parent.apply(this, arguments);
}
Child.prototype = new Parent();
var child = new Child("jin", "1");
child.printName(); // jin
child.sayName(); // Error

var a = new Child();
var b = new Child();
a.list.push("b");
console.log(b.list); // ['a']

优点:可继承父类原型上的属性,且可传参;每个新实例引入的构造函数是私有的
缺点:无论什么情况下,都会调用两次父类构造函数:第一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。子类型对象最终会包含父类型对象的全部实例属性,但不得不在调用子类型构造函数时重写这些属性。

原型式继承

原理:类似 Object.create,用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象,结果是将子对象的__proto__指向父对象:

1
2
3
4
5
6
7
8
9
10
var parent = {
names: ["a"],
};
function createObj(o) {
function F() {}
F.prototype = o;
return new F();
}

var child = createObj(parent);

缺点:共享引用类型

1
2
3
4
5
6
7
8
9
10
11
12
13
var person = {
name: "kevin",
friends: ["daisy", "kelly"],
};

var person1 = createObj(person);
var person2 = createObj(person);

person1.name = "person1";
console.log(person2.name); // kevin

person1.firends.push("taylor");
console.log(person2.friends); // ["daisy", "kelly", "taylor"]

注意:修改 person1.name 的值,person2.name 的值并未发生改变,并不是因为 person1 和 person2 有独立的 name 值,而是因为 person1.name = ‘person1’,给 person1 添加了 name 值,并非修改了原型上的 name 值。

寄生式继承

创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象,相当于二次封装原型式继承,并拓展。

1
2
3
4
5
6
7
8
function createObject(o) {
var clone = Object.create(o);
clone.getNames = function () {
console.log(this.names);
return this.names;
};
return clone;
}

优点:可添加新的属性和方法
缺点:每次创建对象都会创建一遍方法。

寄生组合式继承

原理:改进组合继承,利用寄生式继承的思想继承原型

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
28
29
30
31
32
33
34
35
36
37
38
function copy(object) {
function F() {}
F.prototype = object;
return new F();
}
function inheritPrototype(subClass, superClass) {
// 复制一份父类的原型
var p = copy(superClass.prototype);
// 修正构造函数
p.constructor = subClass;
// 设置子类原型
subClass.prototype = p;
}
// function inheritPrototype(children, parent) {
// // 创建对象
// let prototype = Object.create(parent.prototype);
// // 增强对象
// prototype.constructor = children;
// // 指定对象
// children.prototype = prototype;
// }

function Parent(name, id) {
this.id = id;
this.name = name;
this.list = ["a"];
this.printName = function () {
console.log(this.name);
};
}
Parent.prototype.sayName = function () {
console.log(this.name);
};
function Child(name, id) {
Parent.call(this, name, id);
// Parent.apply(this, arguments);
}
inheritPrototype(Child, Parent);

引用《JavaScript 高级程序设计》中对寄生组合式继承的夸赞就是:

这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

参考文献

https://github.com/mqyqingfeng/Blog/issues/16