文章结构

继承的判断标准
真真假假的继承实现方式

  1. 构造函数绑定
  2. prototype的拷贝
  3. 直接继承prototype
  4. prototype模式
  5. 利用空对象

JavaScript的继承实现设计得有点遮遮掩掩,对于从强类型语言转向来学习JavaScript的新手来说,是件很费脑瓜子的事情。
Sodino作为从Java转向JavaScript的新学员,尝试用这篇文章来理清‘继承’这点事。


继承的判断标准

考虑到JavaScript已经实现了’instanceof’这个运算符,所以本文中约定如下判断标准:

1
2
3
4
5
6
7
8
9
10
11
function Parent() {}
function Child() {}
// -------start------
继承的各种实现方式
// -------end------
var parent = new Parent();
var child = new Child();
chlid instancof Parent == true

chlid instancof Parent值为true时,才判定Child继承自Parent

在此判断标准下,来看看以下各种“百花齐放”的继承实现方式吧…操家伙,割韭菜。


真真假假的继承实现方式

在各种实现方式分为两种思路:

  • 增加Child的属性、方法

    1. 构造函数绑定
  • 操作prototype实现继承关系

    1. prototype拷贝
    2. 直接继承prototype
    3. prototype模式
    4. 利用空对象

下面逐一细说各种方式的实现与结论判断。


构造函数绑定

可以使用Functionapply()call()bind()来绑定构造函数,实现所谓的’继承’效果。
如下代码,child可以执行在Parent类中定义的play()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
代码一:
function Parent() {
this.play = function() {
console.log('play ...');
};
}
function Child(){
Parent.apply(this);
}
var parent = new Parent();
var child = new Child();
// true false
console.log(parent instanceof Parent, parent instanceof Child);
// false true
console.log(child instanceof Parent, child instanceof Child);
child.play(); // print 'play ...'

代码运行如下:

console.1

使用构造函数绑定的方式,对于Chlid()构造函数来说,相当于借用了Parent()函数内的内容来对Child进行属性或方法的定义,在本例中是新增加了play()方法。
与下面的代码是等价的。

1
2
3
4
5
6
function Child() {
// 借用了Parent()中的代码内容
this.play = function() {
console.log('play ...');
};
}

应该知道instancof的运算原理是和对象的原型链相关的,所以构造函数绑定的方式并没有将ParentChild在原型链上建立关系。代码运行后child instancof Parent值是false!!!
所以这种方式只是代码复用的一种技巧,看起来是’继承‘,是假’继承‘。


prototype的拷贝

这种实现方式是将Parent.prototpye中的属性、方法全部复制到Child中去。
实现如下:

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
代码二:
function extendByCopy(Child, Parent) {
  var p = Parent.prototype;
  var c = Child.prototype;
  for (var i in p) {
    c[i] = p[i];
  }
}
function Parent() {
this.play = function() {
console.log('play ...');
};
}
function Child(){}
extendByCopy(Child, Parent);
var parent = new Parent();
var child = new Child();
// true false
console.log(parent instanceof Parent, parent instanceof Child);
// false true
console.log(child instanceof Parent, child instanceof Child);
// child.play() exception..
// 因为extendByCopy()只是修改prototype
// 并没有将Parent私有的方法也复制给Chlid //sodion.com
child.play();

代码运行如下:
console.04

很明显,由于extendByCopy()只是将两个类的prototype经复制后看起来一模一样,但并没有真正在Child的原型链建立与Parent的关系,所以child instanceof Parent值仍为false,所以这也是一种假的’继承‘实现方法。


直接继承prototype

直接继承prototype的方法是将Parent.prototype赋值到Child.prototype,使两者的prototype是一致的。

如下代码中,Child.prototype指向一个新对象,但由于每个prototype都有一个constructor属性,指向它的构造函数,当执行了Child.prototype = Parent.prototype后,
Child.prototype.constructor将会等于Parent,会导致后续通过Child()构造函数初始化的对象的constructor都会是Parent(),这显然会是继承链的紊乱。
所以必须手动纠正,将Child.prototype.constructor赋值为Child本身,以此解决。
这也是JavaScript中务必要遵守的一点,如果替换了prototype对象,则下一步必然是为新的prototype对象加上constructor属性并指回原来的构造函数。

代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Parent() {
this.play = function() {
console.log('play ...');
};
}
function Child(){
}
Child.prototype = Parent.prototype; // Child.prototype指向新对象 // sodino.com
Child.prototype.constructor = Child; // 必须恢复Child.prototype.constructor为Child本身,构造函数不能变
var parent = new Parent();
var child = new Child();
// true true
console.log(parent instanceof Parent, parent instanceof Child);
// true true
console.log(child instanceof Parent, child instanceof Child);
child.play(); // exception...

运行输出如下图:
console.03

这种方式看似符合文章开头对’继承的判断标准’。但真的是‘继承’吗?很明显该方式有以下缺点:
第一继承关系紊乱了。

child instanceof Parent值为true是正常的,但parent instanceof Child值也为true,这…‘乱伦’的画面感不敢看。

第二,由于示例代码中 play()方法并没有声明在Parent.prototype中,所以Child的对象也无法直接调用该方法。

第三,两者的prototype一致了,会导致对任一prototype的改动都会同时反馈在ChlidParent上,而这是不严谨的编程思想。(虽然严谨也不是JavaScript的风格,JavaScript一直都是随随便便的)

第四,在debug界面查看Child的原型链,发现其不完整,缺少了Parent这一环了;而且Parent也被指向了Child,会导致后续调bug时干扰分析思路。

prototype.direct

所以’直接继承prototype’方式,虽然满足child instanceof Parent == true,但这种代码技巧更像是一种‘变脸易容’而已,Sodino也把该方式归为假继承。


prototype模式

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
代码三:
function Parent() {
this.play = function() {
console.log('play ...');
};
}
function Child(){}
Child.prototype = new Parent(); // 子类的prototype对象指向一个父类的实例。
Child.prototype.constructor = Child; // 修正Child的构造函数
var parent = new Parent();
var child = new Child();
// true false
console.log(parent instanceof Parent, parent instanceof Child);
// true true
console.log(child instanceof Parent, child instanceof Child);
child.play(); // print play...

运行后代码如下所示:

console.02

终于child instanceof Parent值为true了。这是一种真正的继承实现方式。
可以在debug界面上观察该child对象的原型链如下图所示:

prototype.mode

相比上文的直接继承prototypeParent的原型链并没有被改变,而且子类的原型链从Child指向Parent再指向Object!很完美!


利用空对象

上文prototype模式已经完美实现继承了。但从代码设计层面上来看,JavaScript中,prototype中声明的属性、方法是共用、共享的,这部分数据被子类是继承是没有问题的。
但父类也有一些自己定义的私有属性、方法,如代码中的play()方法,在JavaScript语言层面上,它并没有定义在Parent.prototype中,所以能不能在实现继承的同时保留该方法仍是父类的私有方法,子类不可访问吗?
答案是可以的。上文prototype模式使用了Parent的一个实例对象,由于该实例对象中有play()方法,所以JavaScript解释器在执行chlid.play()时,发现child本身并没有定义,会顺着原型链逐级向上查找直至找到或找不到抛出异常。在本文示例中,很方便就在Child.prototype,即new Parent()的这个对象中找到了该方法并执行。
所以做出的改进要保留不变的是Child.prototype仍然通过一个对象间接指向Parent.prototype,需要做出改变的是该对象是个空对象即可。
具体实现为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
26
27
28
function extend(Child, Parent) {
  var F = function(){};
  F.prototype = Parent.prototype;
  Child.prototype = new F();
  Child.prototype.constructor = Child;
}
function Parent() {
this.play = function() {
console.log('play ...');
};
}
function Child(){} // sodino.com
extend(Child, Parent);
var parent = new Parent();
var child = new Child();
// true false
console.log(parent instanceof Parent, parent instanceof Child);
// true true
console.log(child instanceof Parent, child instanceof Child);
// exception....
child.play();

运行后效果如下图。

empty.object.console

查看childparent的原型链,仍旧很完美。

prototype.empty

所以这是一种更严格的继承实现方式


About Sodino