[JavaScript]繼承的真真假假

文章結構

繼承的判斷標準
真真假假的繼承實現方式

  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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章