文章结构
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
的属性、方法- 构造函数绑定
- 操作
prototype
实现继承关系prototype
拷贝- 直接继承
prototype
prototype
模式- 利用空对象
下面逐一细说各种方式的实现与结论判断。
构造函数绑定
可以使用Function
的apply()
、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 ...‘ |
代码运行如下:
使用构造函数绑定的方式,对于Chlid()
构造函数来说,相当于借用了Parent()
函数内的内容来对Child
进行属性或方法的定义,在本例中是新增加了play()
方法。
与下面的代码是等价的。
1 2 3 4 5 6 |
function Child() { // 借用了Parent()中的代码内容 this.play = function() { console.log(‘play ...‘); }; } |
应该知道instancof
的运算原理是和对象的原型链相关的,所以构造函数绑定的方式并没有将Parent
与Child
在原型链上建立关系。代码运行后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(); |
代码运行如下:
很明显,由于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... |
运行输出如下图:
这种方式看似符合文章开头对’继承的判断标准’。但真的是‘继承’吗?很明显该方式有以下缺点:
第一继承关系紊乱了。
child instanceof Parent
值为true是正常的,但parent instanceof Child
值也为true,这…‘乱伦’的画面感不敢看。
第二,由于示例代码中 play()
方法并没有声明在Parent.prototype
中,所以Child
的对象也无法直接调用该方法。
第三,两者的prototype
一致了,会导致对任一prototype
的改动都会同时反馈在Chlid
和Parent
上,而这是不严谨的编程思想。(虽然严谨也不是JavaScript的风格,JavaScript一直都是随随便便的)
第四,在debug界面查看Child
的原型链,发现其不完整,缺少了Parent
这一环了;而且Parent
也被指向了Child
,会导致后续调bug时干扰分析思路。
所以’直接继承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... |
运行后代码如下所示:
终于child instanceof Parent
值为true了。这是一种真正的继承实现方式。
可以在debug界面上观察该child
对象的原型链如下图所示:
相比上文的直接继承prototype
,Parent
的原型链并没有被改变,而且子类的原型链从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(); |
运行后效果如下图。
查看child
与parent
的原型链,仍旧很完美。
所以这是一种更严格的继承实现方式。
About Sodino