前言

一开始知道 ES6 引入了 Class(类)这个概念,作为对象的模板,觉得 JavaScript 写类的继承,能更省事省力了,但是在看《你不知道的JavaScript》后,有了更深层次的理解。

ES5 的继承

我们要知道,在继承或者实例化时,JavaScript 的对象机制并不会自动执行复制行为。简单来说,
JavaScript 中只有对象,并不存在可以被实例化的“类”。一个对象并不会被复制到其他对
象,它们会被关联起来。ES5 没有类的概念,要想继承,就必须使用一些技巧。以下介绍 ES5 各种继承。

原型链继承

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
//-------------定义-------------
function Parent(){
this.name = 'hyx';
this.arr = [1,2];
}
Parent.prototype.getName = function(){
return this.name;
}

function Child(){
this.age = 18;
}
Child.prototype = new Parent()//核心
//-------------例子-------------
var child1 = new Child();
var child2 = new Child();

console.log(child1.getName());// hyx
console.log(child2.getName());// hyx

child1.name = 'recall';

console.log(child1.getName());// recall
console.log(child2.getName());// hyx

child1.arr.push(3);

console.log(child1.arr);// [1,2,3]
console.log(child2.arr);// [1,2,3]

原型链继承最大的优点就是:简单,核心就一句话 Child.prototype = new Parent() 但是缺点也在例子中表现出来了,如果父类有引用对象如数组,在子类实例中改变数组,其他子类实例也会跟着变。而且在创建子类实例时,无法向父类构造函数传参。

构造函数继承

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
//-------------定义-------------
function Parent(name){
this.name = name;
this.arr = [1,2];
// 将原型链上的函数转到父类中定义
this.getName = function(){
return this.name;
}
}


function Child(name){
Parent.call(this,name);// 核心
}
//-------------例子-------------
var child1 = new Child('hyx');
var child2 = new Child('recall');

console.log(child1.getName());// hyx
console.log(child2.getName());// recall

child1.name = 'recall';

console.log(child1.getName());// recall
console.log(child2.getName());// recall

child1.arr.push(3);

console.log(child1.arr);// [1,2,3]
console.log(child2.arr);// [1,2]

console.log(child1.getName===child2.getName);// false

构造函数继承解决了原型链继承的两个问题,却又出现了新的问题:每次实例化一个子类,就会重新定义一个函数 getNameconsole.log(child1.getName===child2.getName);// false 说明他们不是复用父类的 getName ,如果实例化多个子类,那内存会爆炸的。

组合继承

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
//-------------定义-------------
function Parent(name){
this.name = name;
this.arr = [1,2];
}
Parent.prototype.getName = function(){
return this.name;
}

function Child(name){
Parent.call(this,name);// 核心,第二次,在实例化的时候又调用一次父类构造函数
}

Child.prototype = new Parent();// 核心,第一次调用父类构造函数
//-------------例子-------------
var child1 = new Child('hyx');
var child2 = new Child('recall');

console.log(child1.getName());// hyx
console.log(child2.getName());// recall

child1.name = 'recall';

console.log(child1.getName());// recall
console.log(child2.getName());// recall

child1.arr.push(3);

console.log(child1.arr);// [1,2,3]
console.log(child2.arr);// [1,2]

console.log(child1.getName===child2.getName);// true

顾名思义,组合继承就是将原型链继承和构造函数继承组合起来,所以核心就是两种继承的核心,而又互补,解决了各自的缺点。但还是有缺点的:子类原型上有一份多余的父类实例属性,因为父类构造函数被调用了两次,生成了两份,而子类实例上的那一份屏蔽了子类原型上的。

原型式继承

网上很多原型式继承都是用到一下这个函数:

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

这个函数本质上是为了给对象创建关联。这段代码使用了一个一次性函数 F ,我们通过改写它的 .prototype 属性使其指向想要关联的对象,然后再使用 new F() 来构造一个新对象进行关联。
我们可以用 Object.create() 来代替上面这段代码,所以原型式继承如下:

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
//-------------定义-------------
function Parent(){
this.name = 'hyx';
this.arr = [1,2];
this.getName = function(){
return this.name;
}
}

var parent = new Parent();
//-------------例子-------------
var child1 = Object.create(parent);// 使用 Object.create()指向parent
var child2 = Object.create(parent);

console.log(child1.getName());// hyx
console.log(child2.getName());// hyx

child1.name = 'recall';

console.log(child1.getName());// recall
console.log(child2.getName());// hyx

child1.arr.push(3);

console.log(child1.arr);// [1,2,3]
console.log(child2.arr);// [1,2,3]

child1.age = 18;
console.log(child1.age);// 18
console.log(child2.age);// undefined;

还有一种方式就是用对象:

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
//-------------定义-------------
var Parent{
name: 'hyx',
arr: [1,2],
getName: function(){
return this.name;
}
}
//-------------例子-------------
var child1 = Object.create(Parent);
var child2 = Object.create(Parent);

console.log(child1.getName());// hyx
console.log(child2.getName());// hyx

child1.name = 'recall';

console.log(child1.getName());// recall
console.log(child2.getName());// hyx

child1.arr.push(3);

console.log(child1.arr);// [1,2,3]
console.log(child2.arr);// [1,2,3]

child1.age = 18;
console.log(child1.age);// 18
console.log(child2.age);// undefined;

和原型链继承一样,继承的属性由所有实例共享,改动一个实例的引用类型值时,所有实例都会改变。还有一个缺点就是无法实现代码复用,因为新对象是现取的,属性是现添的,都没用函数封装,复用不了。

寄生式继承

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
//-------------定义-------------
function create(obj){
var clone = Object.create(obj);
// 在这里添加属性或方法
clone.arr = [1,2];
clone.age = 18;
clone.getAge = function(){
return clone.age;
}
return clone;
}
function Parent(){
this.name = 'hyx';
this.getName = function(){
return this.name;
}
}
//-------------例子-------------
var child1 = create(new Parent);// 实例化
var child2 = create(new Parent);

console.log(child1.getName());// hyx
console.log(child2.getName());// hyx

console.log(child1.getAge === child2.getAge);// false

child1.name = 'recall';

console.log(child1.getName());// recall
console.log(child2.getName());// hyx

child1.arr.push(3);

console.log(child1.arr);// [1,2,3]
console.log(child2.arr);// [1,2]

寄生式继承同样也有对象的写法:

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
//-------------定义-------------
function create(obj){
var clone = Object.create(obj);
// 在这里添加属性或方法
clone.arr = [1,2];
clone.age = 18;
clone.getAge = function(){
return clone.age;
}
return clone;
}
var Parent = {
name : 'hyx',
getName : function(){
return this.name;
}
}
//-------------例子-------------
var child1 = create(Parent);// 这里不一样!
var child2 = create(Parent);

console.log(child1.getName());// hyx
console.log(child2.getName());// hyx

console.log(child1.getAge === child2.getAge);// false

child1.name = 'recall';

console.log(child1.getName());// recall
console.log(child2.getName());// hyx

child1.arr.push(3);

console.log(child1.arr);// [1,2,3]
console.log(child2.arr);// [1,2]

其实 create(obj) 这个函数就是我们子类的定义,我们的子类实例化都是通过这个函数生成的,但是也有缺点,子类里面新增的函数无法实现函数复用。

寄生组合继承

1
2
3
4
5
function inheritPrototype(Child, Parent) {
var prototype = Object.create(Parent.prototype)
prototype.constructor = Child
Child.prototype = prototype
}

该函数实现了寄生组合继承的最简单形式,这个函数接受两个参数,一个子类,一个父类,第一步创建父类原型的副本,第二步将创建的副本添加 constructor 属性,第三步将子类的原型指向这个副本。
然后我们分别定义父类和子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
//-------------定义-------------
function Parent(){
this.name = 'hyx';
this.arr = [1,2];
}
// 函数声明要在父类外
Parent.prototype.getName = function(){
return this.name;
}
function Child(){
Parent.call(this);// 核心
this.age = 18;
}

接着我们使用 inheritPrototype 绑定父类和子类:

1
inheritPrototype(Child,Parent);

最终的例子:

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
function inheritPrototype(Child, Parent) {
var prototype = Object.create(Parent.prototype)
prototype.constructor = Child
Child.prototype = prototype
}
//-------------定义-------------
function Parent(){
this.name = 'hyx';
this.arr = [1,2];
}
// 函数声明要在父类外
Parent.prototype.getName = function(){
return this.name;
}
function Child(){
Parent.call(this);// 核心
this.age = 18;
}
inheritPrototype(Child,Parent);
//-------------例子-------------
var child1 = new Child();
var child2 = new Child();

console.log(child1.getName());// hyx
console.log(child2.getName());// hyx

console.log(child1.getAge === child2.getAge);// true

child1.name = 'recall';

console.log(child1.getName());// recall
console.log(child2.getName());// hyx

child1.arr.push(3);

console.log(child1.arr);// [1,2,3]
console.log(child2.arr);// [1,2]

完美,函数可以复用,子类可以自由定义,而且也没有组合式继承的两次调用父类的问题。除了要将函数用 prototype的形式声明在外面,也没啥缺点了。

ES6 的继承

ES5 那么多继承方式,看的眼花缭乱的,还好,ES6 提供了一个 class 语法糖,新的 class 写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
让我们用 ES6 的 class 来重写上面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Parent{
constructor(name){
this.name = name;
}
getName(){
return this.name;
}
}
class Child extends Parent{
constructor(name,age){
super(name);
this.age = age;
}
toString(){
console.log('Child name is '+super.getName()+' Child age is '+ this.age);
}
}
var child = new Child('hyx',18);
child.toString();// Child name is hyx Child age is 18

这样写就好看多了,函数和定义都在类里面。子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类没有自己的 this 对象,而是继承父类的 this对象,然后对其进行加工。如果不调用 super 方法,子类就得不到 this 对象。

如果子类没有定义 constructor方法,这个方法会被默认添加。

ES5 的继承,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先创造父类的实例对象 this (所以必须先调用 super 方法),然后再用子类的构造函数修改 this

ES6 的 class 有一个缺点,class 不能声明静态属性,(静态属性指的是 Class 本身的属性,即 Class.propName,而不是定义在实例对象(this)上的属性。)

1
2
3
4
5
class Foo {
}

Foo.prop = 1;
Foo.prop // 1

上面的写法为Foo类定义了一个静态属性prop。

目前,只有这种写法可行,因为 ES6 明确规定,Class 内部只有静态方法,没有静态属性。

1
2
3
4
5
6
7
8
9
10
// 以下两种写法都无效
class Foo {
// 写法一
prop: 2

// 写法二
static prop: 2
}

Foo.prop // undefined

但是 Class 却可以有静态方法:

1
2
3
4
5
6
7
8
9
10
11
class Foo {
static classMethod() {
return 'hello';
}
}

Foo.classMethod() // 'hello'

var foo = new Foo();
foo.classMethod();
// TypeError: foo.classMethod is not a function

上面代码中,Foo 类的 classMethod 方法前有 static 关键字,表明该方法是一个静态方法,可以直接在 Foo 类上调用(Foo.classMethod()),而不是在 Foo 类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。

对 Class 的思考

平心而论, class 语法确实解决了典型原型风格代码中许多显而易见的(语法)问题和缺点。然而, class 语法并没有解决所有的问题,在 JavaScript 中使用“类”设计模式仍然存在许
多深层问题。
class 并不会像传统面向类的语言一样在声明时静态复制所有行为。如果你(有意或无意)修改或者替换了父“类”中的一个方法,那子“类”和所有实例都会受到影响,因为它们在定义时并没有进行复制,只是使用基于 [[Prototype]] 的实时委托:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class C {
constructor() {
this.num = Math.random();
}
rand() {
console.log( "Random: " + this.num );
}
}
var c1 = new C();
c1.rand(); // "Random: 0.4324299..."
C.prototype.rand = function() {
console.log( "Random: " + Math.round( this.num * 1000 ));
};
var c2 = new C();
c2.rand(); // "Random: 867"
c1.rand(); // "Random: 432"

此外, class 语法仍然面临意外屏蔽的问题:

1
2
3
4
5
6
7
8
9
10
11
class C {
constructor(id) {
// 噢,郁闷,我们的 id 属性屏蔽了 id() 方法
this.id = id;
}
id() {
console.log( "Id: " + id );
}
}
var c1 = new C( "c1" );
c1.id(); // TypeError -- c1.id 现在是字符串 "c1"

ES6 的 class 最大的问题在于,(像传统的类一样)它的语
法有时会让你认为,定义了一个 class 后,它就变成了一个(未来会被实例化的)东西的静态定义。你会彻底忽略 C 是一个对象,是一个具体的可以直接交互的东西。
在传统面向类的语言中,类定义之后就不会进行修改,所以类的设计模式就不支持修改。但是 JavaScript 最强大的特性之一就是它的动态性,任何对象的定义都可以修改(除非你
把它设置成不可变)。

我们通过 ES5 和 ES6 的 class 继承的对比,可以知道,其实 JavaScript 的继承,更应该说是通过原型链进行委托。
委托行为意味着某些对象在找不到属性或者方法引用时会把这个请求委托给另一个对象。这是一种极其强大的设计模式,和父类、子类、继承、多态等概念完全不同。
对象并不是按照父类到子类的关系垂直组织的,而是通过任意方向的委托关联并排组织的。

参考:
JavaScript继承(图解笔记)
重新理解JS的6种继承方式
Class 的继承
《你不知道的JavaScript》