Javascript繼承-原型的陷阱

2018-06-16 18:34 更新

在學(xué)習(xí)javascript的過(guò)程中,許多新手發(fā)現(xiàn)很難弄明白javascript復(fù)雜的的原型繼承工作機(jī)制。在這篇文章中我談?wù)勗谕ㄟ^(guò)父函數(shù)的原型繼承模型中如何實(shí)現(xiàn)實(shí)例屬性。

一個(gè)簡(jiǎn)單的Widget 對(duì)象

在下面的代碼中,我們有個(gè)一父類 Widget,父類有個(gè)屬性 messages和父類為Widget的SubWidget類。在這種情況下我們想讓SubWidget的每個(gè)實(shí)例在初始化的時(shí)候一個(gè)空的消息數(shù)組:

var Widget = function( name ){
   this.messages = [];
};

Widget.prototype.type='Widget';

var SubWidget = function( name ){
  this.name = name;
  Widget.apply( this, Array.prototype.slice.call( arguments ) );
};

SubWidget.prototype = new Widget();

在我們?cè)O(shè)置SubWidget 的原型為Widget的一個(gè)實(shí)例之前,對(duì)象的關(guān)系圖如下:

代碼最后一行將SubWidget的父類設(shè)置為Widget類的一個(gè)實(shí)例,”new”關(guān)鍵字背后,創(chuàng)建了繼承樹(shù)并且綁定了對(duì)象的原型鏈,現(xiàn)在我們的對(duì)象關(guān)系圖看起來(lái)像下面這樣:

你看出問(wèn)題所在了嗎?讓我們創(chuàng)建子類的實(shí)例凸顯問(wèn)題:

var sub1 = new SubWidget( 'foo' );
var sub2 = new SubWidget( 'bar' );

sub1.messages.push( 'foo' ); 
sub2.messages.push( 'bar' );

現(xiàn)在我們的對(duì)象關(guān)系圖看起來(lái)像這樣:

在談?wù)撜嬲膯?wèn)題之前,我想想退一步,先談?wù)剋idget構(gòu)造函數(shù)中的屬性(type),如果在實(shí)例初始化過(guò)程中沒(méi)有初始化屬性(type)那實(shí)際上這個(gè)屬性存在widget構(gòu)造函數(shù)中(實(shí)際上存在wedget的實(shí)例中,也就是subwidget實(shí)例的原型中)。然而,一旦在(子類實(shí)例)初始化過(guò)程中屬性被賦予新值,如 sub1.type = ‘Fuzzy Bunny’,它將變成實(shí)例的屬性,如圖所示:

思考問(wèn)題

我們的bug開(kāi)始變得很清晰,讓我們輸出sub1和sub2的messages數(shù)組:

var Widget = function(){
   this.messages = [];
};

Widget.prototype.type='Widget';

var SubWidget = function( name ){
  this.name = name;
};

SubWidget.prototype = new Widget();

var sub1 = new SubWidget( 'foo' );
var sub2 = new SubWidget( 'bar' );

sub1.messages.push( 'foo' ); 
sub2.messages.push( 'bar' );

console.log( sub1.messages ); //[ 'foo', 'bar' ]
console.log( sub2.messages ); //[ 'foo', 'bar' ]

如果你運(yùn)行這段代碼,在你的控制臺(tái)將出現(xiàn)2個(gè)重復(fù) [“foo”, “bar”]。每個(gè)對(duì)象共享相同的messages數(shù)組。

解決問(wèn)題

最容易想到的辦法,我們可以給SubWidget構(gòu)造函數(shù)添加新屬性,如下所示:

var SubWidget = function( name ){
  this.name = name;
  this.messages = [];
};

然而,如果我們想創(chuàng)建其他繼承自Widget的對(duì)象呢?新對(duì)象也要添加消息數(shù)組。很快維護(hù)和擴(kuò)展我們的代碼將變成一場(chǎng)噩夢(mèng)。另外,如果我們想給Widget構(gòu)造函數(shù)添加其他屬性,我們?nèi)绾螌⑦@些屬性編程子類的實(shí)例屬性?這種方法是不可重用的和不夠靈活。

為了妥善解決這個(gè)問(wèn)題,需要給我們的SubWidget構(gòu)造函數(shù)添加一行代碼,調(diào)用Widget構(gòu)造函數(shù)并且傳入SubWidget構(gòu)造函數(shù)的作用域。為此我們要用apply()方法,可以靈活的無(wú)副作用的將SubWidget構(gòu)造函數(shù)的arguments傳入Widget構(gòu)造函數(shù)中。

var Widget = function(){
   this.messages = [];
};

Widget.prototype.type='Widget';

var SubWidget = function( name ){

  this.name = name;

  Widget.apply( this, Array.prototype.slice.call(arguments) );
};

SubWidget.prototype = new Widget();

apply()方法可以讓我們可以將messages數(shù)字的作用域更改為SubWidget的實(shí)例。現(xiàn)在我們創(chuàng)建的每一個(gè)實(shí)例對(duì)象都有一個(gè)實(shí)例messages 數(shù)組。

var Widget = function( ){
   this.messages = [];
};

Widget.prototype.type='Widget';

var SubWidget = function( name ){

  this.name = name;
  Widget.apply( this, Array.prototype.slice.call( arguments ) );
};

SubWidget.prototype = new Widget();

var sub1 = new SubWidget( 'foo' );
var sub2 = new SubWidget( 'bar' );

sub1.messages.push( 'foo' );

sub2.messages.push( 'bar' );

console.log(sub1.messages); // ['foo']
console.log(sub2.messages); // ['bar']

運(yùn)行上面的代碼,你將看見(jiàn) [“foo”] 和 [“bar”] ,因?yàn)槲覀兊膶?duì)象實(shí)例現(xiàn)在有自己的messages數(shù)組屬性。

現(xiàn)在我們的對(duì)象關(guān)系圖如下:

譯者補(bǔ)充

上面的繼承方式是借用構(gòu)造函數(shù)模式,《javascript patterns》中有詳細(xì)介紹,作者寫(xiě)的很詳細(xì)了,但有2個(gè)小問(wèn)題在此補(bǔ)充:

1

var SubWidget = function( name ){

  this.name = name;
  Widget.apply( this, Array.prototype.slice.call( arguments ) );
};

作者的代碼中父類會(huì)覆蓋子類的屬性,這有悖于重構(gòu)的概念,稍加改變即可,在子類構(gòu)造函數(shù)中先調(diào)用父類構(gòu)造函數(shù),這相當(dāng)于java中的super:

var SubWidget = function( name ){
  Widget.apply( this, Array.prototype.slice.call( arguments ) );
  this.name = name;
};

2

var SubWidget = function( name ){

  this.name = name;
  Widget.apply( this, Array.prototype.slice.call( arguments ) );
};

SubWidget.prototype = new Widget();

父類的屬性被初始化了2次,一次是借用構(gòu)造函數(shù),一次是new Widget(),造成浪費(fèi),稍加改變即可:

var SubWidget = function( name ){

  this.name = name;
  Widget.apply( this, Array.prototype.slice.call( arguments ) );
};

SubWidget.prototype = Widget.prototype;

關(guān)于繼承我還寫(xiě)了另一篇文章,可以完美解決上面的所有問(wèn)題《JavaScript對(duì)象繼承一瞥》。

英文原文:”JavaScript Inheritance – How To Shoot Yourself In the Foot With Prototypes!

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)