全面了解 Javascript Prototype Chain 原型鏈

  • 2021 年 12 月 2 日
  • 筆記

原型鏈可以說是Javascript的核心特徵之一,當然也是難點之一。學過其它面向對象的程式語言後再學習Javascript多少會感到有些迷惑。雖然Javascript也可以說是面向對象的語言,但是其實現面向對象是通過prototype-based的機制而不是class-based機制。它沒有其它面向對象語言的繼承,多態等,但是我們卻可以通過prototype來實現繼承。

下面我就帶大家來了解一下原型鏈

原型鏈初接觸

之前寫過《你理解Javascript的閉包嗎》這篇文章,在介紹閉包的時候首先是從其名字開始介紹的,通過名字大概了解其涉及的知識點。同樣在這我們也先從這個名字開始談起。
原型鏈,可以分成兩部分——原型和鏈。

對於Javascript,可以看作是一個自上而下的鏈式結構。(如果對Javascript基礎知識比較欠缺,可以參考 Javascript 教程)我們來看這樣的一個例子

例一

function func(){
   this.a = '1';
   this.b = '2';
}
func.prototype.b='3';
func.prototype.c='4';
var f = new func();
console.log(f.a);
console.log(f.b);
console.log(f.c);

 

對於這個例子先用下圖表示其各屬性之間的關係

圖一

 

此種鏈式結構首先會查找func()實例化對象,沒有找到要找的數據的話就會再去查找其原型Prototype。所以我們來看上面的例子,f.a在對象f中存在,所以直接列印為1;f.c在對象f中不存在,所以再去查找原型Prototype,所以c列印為4;對於f.b,在對象f和原型Prototype中都存在,但是按照其查找順序,首先找到的是對象f中的b,所以就直接列印為2。這就是Javascript鏈式結構的查找機制。

那對於Prototype中的b豈不是就浪費了,對於Prototype中的b我們稍後會解釋它在什麼情況下會被用到。

下面我們再來看一下原型的概念。

原型

對於原型——也就是Prototype,指的是生成對象的那個方法。在其他程式語言中我們可以理解成類,而Prototype可以認為是static靜態屬性,它是屬於類而不是屬於對象的,但是其又可以被對象調用。

既然叫原型,那它肯定是在類下面而不是對象,所以對於上例來說我們可以寫成func.prototype.c = 『4』 卻不可以寫成 f.prototype.c = 『4』,後者是有語法錯誤的,js調試器會報f.prototype is undefined錯誤。也就是說f.prototype是錯誤的,當然也不能說是錯誤,f.prototype可以看成是prototype是對象f的一個屬性,和f.a、f.b沒有任何的區別。

所以說從名稱上面我們就可以大概了解其原理,從而避免一些不必要的錯誤。

原型鏈

上面分別介紹了原型和鏈兩個概念以後,現在我們將二者結合起來看一下原型鏈以及如何構造一個原型鏈。

我們可以這樣認為,Javascript對象就是一個屬性包(當然這些屬性都是其自身的屬性),並且對象本身還有一條指向其原型的鏈。當試圖訪問對象的某個屬性的時候,它不僅僅在對象本身查找,還會去對象指向的原型上面去查找,以及該對象原型的原型,依次層層向上搜索,直到找到一個名字匹配的屬性抑或是達到原型鏈的末尾。

還記得上面例一中提到的func.prototype.b這個屬性嗎,我們對例一進行一下改造

例二

function func(){
   this.a = '1';
   this.b = '2';
}
func.prototype.b = '3';
func.prototype.c = '4';
var f = new func();
 
function func2(){
   this.d = '5';
}
func2.prototype = Object.create(func.prototype);
func2.prototype.c = '6';
var f2 =new func2();
console.log(f.a);
console.log(f2.b);
console.log(f2.d);
console.log(f2.c);
console.log(f.c);

 

對於這個例子,我們先用一張圖來表示其各部分關係

圖二

 

結合上圖按照Javascript訪問某個對象屬性的查找順序我們可以推出上面各個值。

f.a 毫無疑問值為1;f2.b首先會查找對象f2,沒有此屬性然後再去查找其原型依然沒有,接著再去查找其原型所指的原型發現b=3,所以f2.b列印值為3;f2.d 值為5,這個沒有什麼疑問;我們先說f.c,它和例一中的情況是一樣的,在f對象所指的原型中定義為4,所以在本例中同樣f.c的值為4;最後是f2.c,我們看f2的原型指向了f的原型,但是在下面有對f2的原型func2.prototype中的屬性c進行了定義即func2.prototype.c=6 所以說f2.c首先在f2的原型中被找到從而列印出來,並不會繼續向上一層找,所以f2.c列印值為6,如果說將func2.prototype.c=6注釋掉,那f2.c的值其實就是f對象所指原型中的c的值了,也就是4;那雖然說f2的原型指向了f的原型,那對f2的原型增加屬性會不會影響f的原型,答案是否定的,也就是說f指向的原型不會受影響。因為我們可以看作是Object.create(func.prototype)實例化了一個f指向的原型的一個對象並將其賦值給f2的原型,所以f的原型本身是不會受影響的。這一點從f.c的值也可以看出來,如果受影響的話那f.c的值不就成了6了嗎,可是事實卻不是這樣的。

通過這個例子相信大家對原型鏈的概念應該有一個基本的認識吧!

原型鏈實際應用

我們可以使用原型鏈來模擬面向對象程式語言中的繼承,例子如下

例三

function Action(){
   this.pro1 = 'a';
}
Action.prototype = {
   pro2:'b',
   operate:function(){
      console.log('action');
   }
}
function IndexAction(){
   this.pro1 = 'b';
}
IndexAction.prototype = Object.create(Action.prototype);
var index = new IndexAction();
index.operate();

 

這個例子可以看成是一個簡單的繼承關係,其中Action中的pro1是其私有的屬性,不會被IndexAction所繼承,但是其pro2和方法operate會被繼承。而IndexAction中的pro2也是其自己的私有屬性。

當然原型鏈還有在其他方面的應用,這裡我們就舉一個繼承的例子來供大家理解。

原型鏈的性能

當訪問某一對象的屬性時,在原型鏈上查找該屬性時比較費時間的,這堆性能有比較壞的影響。這在對性能要求比較苛刻的情況下是比較重要的。除此之外,當我們訪問一個不存在的屬性的時候,Javascript會遍歷整個原型鏈。因此我們在用原型編寫複雜程式碼前充分了解原型繼承的構造是非常有必要的,並且掌握自己程式碼中原型鏈的長度在必要的時候結束原型鏈從而避免上面提到的性能問題。

此外對於Javascript中原生對象的原型不要在上面進行新功能的擴展,除非是為了Javascript的兼容性。

例如

Object.prototype = {
         pro:」pro」,
         operate:function(){}
}

 

Object是Javascript中原生的對象,我們要盡量避免上述程式碼出現在我們的程式中。

總之,對於原型鏈我們應該多在編程過程中去理解,否則即使當時明白,經過一段時間不去寫程式碼的話慢慢的我們也就會淡忘。