天天看點

【轉】JavaScript圖解繼承(多圖)原型鍊繼承使用原型鍊繼承的尴尬借用構造函數改進原型鍊繼承構造函數+原型鍊繼承的贅餘與改進方法

轉自:http://www.2cto.com/kf/201604/498730.html

在JavaScript中,繼承主要是通過原型鍊來實作的。原型鍊和前文所說的原型對象密切相關。原型對象可以參考JavaScript構造函數和原型對象。為了徹底搞清楚JavaScript的繼承,我們先搞清楚原型鍊是什麼。

原型鍊繼承

我們知道,所有的引用類型都預設繼承了Object,因而所有自定義類型都擁有toString()、valueOf等預設方法。我們隻是知道這個結論,但現在我們更感興趣的是這個繼承關系究竟是如何實作的。

我們先來回顧一下構造函數,原型對象和執行個體的關系:

每個構造函數都有一個原型對象,并包含指向原型對象的指針; 每個原型對象都包含一個指向構造函數的指針; 每個執行個體内部都有一個指向原型對象的指針;

大概可以用以下的關系圖來描述:

【轉】JavaScript圖解繼承(多圖)原型鍊繼承使用原型鍊繼承的尴尬借用構造函數改進原型鍊繼承構造函數+原型鍊繼承的贅餘與改進方法

 現在,我們來設想一下,如果我們讓構造函數B的原型對象等于另一個類型A的執行個體會發生什麼?

上面說了,每個執行個體中都含有一個指向原型對象的指針。相應地,如果讓構造函數B的原型對象等于執行個體A,這個指針便會存在于構造函數B的原型對象中,指向A的原型對象,我們用圖二表示這種關系:

【轉】JavaScript圖解繼承(多圖)原型鍊繼承使用原型鍊繼承的尴尬借用構造函數改進原型鍊繼承構造函數+原型鍊繼承的贅餘與改進方法

 如圖二所示,構造函數B的原型對象被重寫為A的執行個體,則原本存在于A執行個體中的所有屬性以及方法都會存在于構造函數B的原型對象中。這樣我們便可以通過

prototype

的索引查詢到原本存在于類型A中的屬性和方法了。由此便可實作對A的繼承!我們不妨看一個例子:

function ConsA(name) {
  this.name = name;
}
 
ConsA.prototype.alertName = function() {
  alert(this.name);
}
 
function ConsB(name) {
  this.name = name;
}
 
//通過将構造函數ConsB的原型對象指定位A的一個執行個體實作繼承
var a1 = new ConsA("A");
ConsB.prototype = new a1;
 
var b1 = new ConsB("B");
 
a1.alertName();  //A
b1.alertName();  //B      

 以上代碼首先建立一個構造函數ConsA,然後在該構造函數的原型對象中添加一個alertName()方法。

接着建立構造函數ConsB,然後通過将構造函數ConsB的原型對象指定為A的一個執行個體實作對A的繼承。

分别建立執行個體a1以及b1,我們可以看到b1繼承了ConsA原型對象中的alertName()方法。

新建立的執行個體B中包含指向原型對象B的指針,實質是指向一個A執行個體。執行個體A中包含一個指向A的原型對象的指針,則原型對象B中也包含一個指向原型對象A的指針。如果原型對象A又是另一類型的執行個體,這樣層層遞進,就形成了執行個體、原型的鍊條——原型鍊。

JS中的繼承主要就是依靠原型鍊來實作的。所謂所有引用類型都預設繼承了Object,實質上所有函數的預設原型都是Object執行個體,該執行個體中包含一個指向Object原型對象的指針。在圖二中加入該繼承層次,完整的繼承鍊應如下圖所示:

【轉】JavaScript圖解繼承(多圖)原型鍊繼承使用原型鍊繼承的尴尬借用構造函數改進原型鍊繼承構造函數+原型鍊繼承的贅餘與改進方法

 由此,便實作了對Object的預設繼承。

當執行個體B需要調用valueOf()方法時。首先會在執行個體B中搜尋是否存在該執行個體方法,不存在;通過[[prototype]]到原型對象B中繼續搜尋,不存在;再通過[[prototype]]到原型對象A中搜尋,不存在;繼續沿着原型鍊到Object原型對象中搜尋,存在。如果一直未找到需要的屬性或方法,會沿着原型鍊一直搜尋下去,直到最後一層!

使用原型鍊繼承的尴尬

在JavaScript構造函數和原型對象中已經說過,使用原型對象在資料共享方面有得天獨厚的優勢。然而正是這種優勢有的時候也會成為劣勢,問題就在于,很多時候,我們并不希望所有的屬性都被共享,尤其對于引用類型的屬性。

假設執行個體A中擁有一個數組變量

nums = ['1', '2', '3'];      

 構造函數B在繼承執行個體A時會将該變量也添加在構造函數B的原型對象中,那麼我們在對執行個體B繼承的nums進行操作的時候,如

insB.nums.push("4")

,這個修改同樣會導緻執行個體A的nums被篡改掉,也就會導緻所有B類型執行個體中的nums值發生改變!如下圖藍色部分所示(省略了原型鍊中Object層):

【轉】JavaScript圖解繼承(多圖)原型鍊繼承使用原型鍊繼承的尴尬借用構造函數改進原型鍊繼承構造函數+原型鍊繼承的贅餘與改進方法

 原因就是因為nums是一個引用類型的變量,他們都是指向同一塊記憶體位置的指針。

特别注意!以上将原型對象B和執行個體A分開畫成兩個部分是便于直覺了解,實質上他們是對同一塊記憶體的引用。

借用構造函數改進原型鍊繼承

我們曾經說過,構造函數和普通函數沒有什麼本質的差別。差別就在于調用方式,如果對一個所謂的普通函數使用new來調用,那麼這個函數就是構造函數;對所謂的構造函數不使用new來調用,它就是普通函數。函數隻不過是在特定環境中執行代碼的對象,我們假設有如下構造函數A:

function ConsA(name, peers) {
    this.name = name;
    this.peers = peers;
}      

 我們用如下的方式,在構造函數B中調用ConsA:

function ConsB(name, peers, age) {
  ConsA.call(this, name, peers);
  this.age = age;
}      

 這裡沒有使用new操作符,實質上就相當于在ConsB的環境中将普通函數ConsA的代碼執行了一遍。由此在調用構造函數ConsB建立B執行個體的時候,每個執行個體都會獲得name,peers副本作為自己的執行個體屬性,例如當執行

insB = new ConsB("B", ['1', '2', '3'], 20);

時,insB會獲得自己的屬性副本:

【轉】JavaScript圖解繼承(多圖)原型鍊繼承使用原型鍊繼承的尴尬借用構造函數改進原型鍊繼承構造函數+原型鍊繼承的贅餘與改進方法

 我們将以上方法加入到原型鍊繼承當中,注意代碼中的注釋部分:

代碼2

function ConsA(name, peers) {
  this.name = name;
  this.peers = peers;
}
 
ConsA.prototype.alertName = function() {
  alert(this.name);
}
 
function ConsB(name, peers, age) {
  ConsA.call(this, name, peers);
  this.age = age;
}
 
//這裡先建立一個A的執行個體a1再将該執行個體賦給ConsB的prototype
var a1 = new ConsA("A", ["1", "2", "3"]);
ConsB.prototype = a1;
 
//建立B的執行個體b1然後在b1的引用屬性peers中添加新值"4"
var b1 = new ConsB("B", ["1", "2", "3"], 20);
b1.peers.push("4");
 
b1.alertName();  //該方法從A的原型對象繼承而來
//對B中引用屬性的修改沒有改變執行個體A中的屬性
alert("b1.peers : " + b1.peers + "\n" + "a1.peers : " + a1.peers);        

 代碼執行結果:

【轉】JavaScript圖解繼承(多圖)原型鍊繼承使用原型鍊繼承的尴尬借用構造函數改進原型鍊繼承構造函數+原型鍊繼承的贅餘與改進方法
【轉】JavaScript圖解繼承(多圖)原型鍊繼承使用原型鍊繼承的尴尬借用構造函數改進原型鍊繼承構造函數+原型鍊繼承的贅餘與改進方法

 以上代碼繼承的結果如下圖所示:

【轉】JavaScript圖解繼承(多圖)原型鍊繼承使用原型鍊繼承的尴尬借用構造函數改進原型鍊繼承構造函數+原型鍊繼承的贅餘與改進方法

 這樣,每個B執行個體都獲得了一份A的屬性副本作為自己的執行個體屬性。該屬性也就屏蔽掉了原型對象中的同名屬性。各個執行個體擁有自己的副本,對資料的操作彼此之間互不影響。解決了屬性共享方面的尴尬。

構造函數+原型鍊繼承的贅餘與改進方法

在借用構造函數和原型鍊組合繼承的方法中,我們通過構造函數繼承屬性,通過原型繼承方法。方法可以共享;對于屬性,每個執行個體都擁有一份自己的副本互不幹擾。看起來已經高枕無憂,但是我們仔細觀察圖六。不難發現A的屬性不僅在執行個體B中建立了一份,在B的原型中也建立了一份!我們隻不過是使用同名的屬性将原型對象B中的屬性覆寫掉了。但是它們依然是存在的。這顯然是多餘的。

事實上,我們使用原型對象是想繼承A類型的方法,A類型的方法存在于A的原型對象中,是以我們真正需要的不過是原型對象A的一個副本而已!既然如此,何必指定一個A的執行個體對象作為原型對象呢?

我們不妨這樣做,建立A原型對象的一份副本,對其做一定的修改,保持原型鍊不變。然後将其指定為B的原型對象以實作方法的繼承。

以下方法由道格拉斯·克羅克福德提出推廣。

首先建立一個基于已有對象建立新對象的函數:

function object(o) {
  function F(){};
  F.prototype = o;
  return new F();
}      

 借用這個函數,我們來建立另外一個函數,實作一個構造B對另一個構造A的原型對象的繼承,并保持原型鍊不變:

fucntion inheritPrototype(B, A) {
  var o = object(A.prototype);
  o.constructor = B;
  B.prototype = o;
}      

 以上代碼很簡單,資訊量還是很豐富的。

首先,第二行,借用我們上面的object()函數,建立了一個基于A的原型對象的對象副本。什麼意思呢?就是建立了一個對象,并将這個對象的原型對象指定為A的原型對象。然後把這個對象指派給局部變量o。 第三行,将利用object()函數傳回的對象o的constructor指回B。 第四行,将調整好的o指定為B的原型對象。

這個過程比較抽象,請看以下圖示:

【轉】JavaScript圖解繼承(多圖)原型鍊繼承使用原型鍊繼承的尴尬借用構造函數改進原型鍊繼承構造函數+原型鍊繼承的贅餘與改進方法

 我們主要修改的位置就是原型對象B,我們相當于建立了一個空的構造函數,将其原型對象指向A的原型對象。然後建立該構造函數的執行個體作為原型對象B。這樣就省去了在原型對象B中包含一大堆A中屬性的贅餘。得到了漂亮的繼承。

利用以上方法改進代碼2如下:

代碼3

function ConsA(name, peers) {
  this.name = name;
  this.peers = peers;
}
 
ConsA.prototype.alertName = function() {
  alert(this.name);
}
 
function ConsB(name, peers, age) {
  ConsA.call(this, name, peers);
  this.age = age;
}
 
inheritPrototype(ConsB, ConsA);
 
//建立B的執行個體b1
var b1 = new ConsB("B", ["1", "2", "3"], 20);      

 至此,繼承已經算是十分理想了。^_^

繼續閱讀