Javascript繼承-原型的陷阱

2018-06-16 18:34 更新

在學習javascript的過程中,許多新手發(fā)現(xiàn)很難弄明白javascript復雜的的原型繼承工作機制。在這篇文章中我談談在通過父函數(shù)的原型繼承模型中如何實現(xiàn)實例屬性。

一個簡單的Widget 對象

在下面的代碼中,我們有個一父類 Widget,父類有個屬性 messages和父類為Widget的SubWidget類。在這種情況下我們想讓SubWidget的每個實例在初始化的時候一個空的消息數(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();

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

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

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

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

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

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

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

思考問題

我們的bug開始變得很清晰,讓我們輸出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' ]

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

解決問題

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

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

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

為了妥善解決這個問題,需要給我們的SubWidget構(gòu)造函數(shù)添加一行代碼,調(diào)用Widget構(gòu)造函數(shù)并且傳入SubWidget構(gòu)造函數(shù)的作用域。為此我們要用apply()方法,可以靈活的無副作用的將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的實例。現(xiàn)在我們創(chuàng)建的每一個實例對象都有一個實例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']

運行上面的代碼,你將看見 [“foo”] 和 [“bar”] ,因為我們的對象實例現(xiàn)在有自己的messages數(shù)組屬性。

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

譯者補充

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

1

var SubWidget = function( name ){

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

作者的代碼中父類會覆蓋子類的屬性,這有悖于重構(gòu)的概念,稍加改變即可,在子類構(gòu)造函數(shù)中先調(diào)用父類構(gòu)造函數(shù),這相當于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(),造成浪費,稍加改變即可:

var SubWidget = function( name ){

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

SubWidget.prototype = Widget.prototype;

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

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

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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號