JavaScript 深入继承的六种方式和优缺点

作者按:

关于JavaScript 继承的几种方式,网上有很多文章。但其实红宝书里已经讲解的非常透彻,思路清晰。本篇文章更像是学习笔记,第八章常读常新。

红宝书第八章继承-from 微信读书


以下是正文:

1. 原型链继承

1
2
3
4
5
6
7
8
9
10
11
12
13
function Parent () {
this.name = 'yang';
}

Parent.prototype.getName = function () {
console.log(this.name);
}
function Child () {

}
Child.prototype = new Parent();
var child1 = new Child();
console.log(child1.getName()) // yang

问题1:引用类型的属性被所有实例共享
问题2:在创建 Child实例时,不能像Parent传参

2. constructor stealing (盗用构造函数)

1
2
3
4
5
6
7
8
9
10
11
function Parent () {
this.names = ['yang', 'yao'];
}
function Child () {
Parent.call(this);
}
var child1 = new Child();
child1.names.push('xuezhuo');
console.log(child1.names); // ["yang", "yao", "xuezhou"]
var child2 = new Child();
console.log(child2.names); // ["yang", yao"]

优点:
1.避免了引用类型的属性被所有实例共享
2.可以在 Child 中向 Parent 传参
缺点:
方法都在构造函数中定义,每次创建实例都会创建一遍方法。

3. 组合继承(经典继承)

原型链继承和经典继承的双剑合璧,基本的思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。

弥补了原型链和盗用构造函数的不足,是 JavaScript 中使用最多的继承模式

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
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;

var child1 = new Child('yang', '18');
child1.colors.push('black');

console.log(child1.name); // yang
console.log(child1.age); // 18
console.log(child1.colors); // ["red", "blue", "green", "black"]

var child2 = new Child('miemie', '20');

console.log(child2.name); // miemie
console.log(child2.age); // 20
console.log(child2.colors); // ["red", "blue", "green"]

4. 原型式继承

1
2
3
4
5
function createObj(o) {
function F(){}
F.prototype = o;
return new F();
}

这个函数是 Douglas Crockford 给出的,适用于在一个对象的基础上在创造一个对象。
ECMAScript 5通过增加Object.create()方法将原型式继承的概念规范化了。
但是它的缺点还是跟原型链继承一样,属性中包含的引用值还是会在对象中共享。

5. 寄生式继承

创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。

1
2
3
4
5
6
7
function createObj (o) {
var clone = Object.create(o);
clone.sayName = function () {
console.log('hi');
}
return clone;
}

但是它还是有跟构造函数模式一样的缺点

6. 终极法器:寄生组合式继承

先来复习一下组合继承的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent();
var child1 = new Child('kevin', '18');
console.log(child1)

组合继承最大的缺点是会调用两次父构造函数。

一次是设置子类型实例的原型的时候:

1
Child.prototype = new Parent();

一次在创建子类型实例的时候:

1
var child1 = new Child('yang', '18');

回想下 new 的模拟实现,其实在这句中,我们会执行:

1
Parent.call(this, name);

在这里,我们又会调用了一次 Parent 构造函数。

所以,在这个例子中,如果我们打印 child1 对象,我们会发现 Child.prototype 和 child1 都有一个属性为colors,属性值为[‘red’, ‘blue’, ‘green’]。

那么我们该如何精益求精,避免这一次重复调用呢?

如果我们不使用 Child.prototype = new Parent() ,而是间接的让 Child.prototype 访问到 Parent.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
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
console.log(this.name)
}

function Child (name, age) {
Parent.call(this, name);
this.age = age;
}

// 关键的三步
var F = function () {};

F.prototype = Parent.prototype;

Child.prototype = new F();


var child1 = new Child('yang', '18');

console.log(child1);

最后我们封装一下这个继承方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function object(o) {
function F() {}
F.prototype = o;
return new F();
}

function prototype(child, parent) {
var prototype = object(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}

// 当我们使用的时候:
prototype(Child, Parent);

寄生式组合继承可以算是引用类型继承的最佳模式。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!