Javascript 繼承-原型的陷阱

注:本文爲翻譯文章,原文爲"JavaScript Inheritance – How To Shoot Yourself In the Foot With Prototypes!"javascript

在學習javascript的過程當中,許多新手發現很難弄明白javascript複雜的的原型繼承工做機制。在這篇文章中我談談在經過父函數的原型繼承模型中如何實現實例屬性。java

一個簡單的Widget 對象

在下面的代碼中,咱們有個一父類 Widget,父類有個屬性 messages和父類爲Widget的SubWidget類。在這種狀況下咱們想讓SubWidget的每一個實例在初始化的時候一個空的消息數組:web

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();

在咱們設置SubWidget 的原型爲Widget的一個實例以前,對象的關係圖以下:編程

代碼最後一行將SubWidget的父類設置爲Widget類的一個實例,"new"關鍵字背後,建立了繼承樹而且綁定了對象的原型鏈,如今咱們的對象關係圖看起來像下面這樣:數組

你看出問題所在了嗎?讓咱們建立子類的實例凸顯問題:app

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

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

如今咱們的對象關係圖看起來像這樣:函數

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

思考問題

咱們的bug開始變得很清晰,讓咱們輸出sub1和sub2的messages數組:this

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' ]

若是你運行這段代碼,在你的控制檯將出現2個重複 ["foo", "bar"]。每一個對象共享相同的messages數組。spa

解決問題

最容易想到的辦法,咱們能夠給SubWidget構造函數添加新屬性,以下所示:

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

然而,若是咱們想建立其餘繼承自Widget的對象呢?新對象也要添加消息數組。很快維護和擴展咱們的代碼將變成一場噩夢。另外,若是咱們想給Widget構造函數添加其餘屬性,咱們如何將這些屬性編程子類的實例屬性?這種方法是不可重用的和不夠靈活。

爲了妥善解決這個問題,須要給咱們的SubWidget構造函數添加一行代碼,調用Widget構造函數而且傳入SubWidget構造函數的做用域。爲此咱們要用apply()方法,能夠靈活的無反作用的將SubWidget構造函數的arguments傳入Widget構造函數中。

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數字的做用域更改成SubWidget的實例。如今咱們建立的每個實例對象都有一個實例messages 數組。

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"] ,由於咱們的對象實例如今有本身的messages數組屬性。

如今咱們的對象關係圖以下:

譯者補充

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

1

var SubWidget = function( name ){

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

做者的代碼中父類會覆蓋子類的屬性,這有悖於重構的概念,稍加改變便可,在子類構造函數中先調用父類構造函數,這至關於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次,一次是借用構造函數,一次是new Widget(),形成浪費,稍加改變便可:

var SubWidget = function( name ){

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

SubWidget.prototype = Widget.prototype;

若是你以爲我翻譯的不錯,能夠在這裏關注個人微博

相關文章
相關標籤/搜索