說來話長的 Javascript 原型鏈

七姊薔薇發表於2019-01-20

為了不被罰200塊錢,不得不強迫自己在今天之前寫完這篇部落格,人就是要對自己狠一點,想些招數來強迫自己,也許是件好事。

JS的原型鏈總是被端上前端面試桌上的一盤經典菜,不同的人從不同的角度去品鑑。今天我想從建構函式模式到原型模式再到原型鏈來闡述我的理解。

建構函式模式

以前的我只是知道建構函式就是定義一個函式,函式名大寫,函式裡面給隱式返回的this物件新增屬性和方法,這個函式就是建構函式。

至於為什麼要用建構函式呢?就有些迷茫了,所以再次翻開了紅包書那些曾經令人頭疼的物件導向章節(不得不說JS高階程式設計真是一本好書)

為什麼要用建構函式?

首先因為物件導向的程式設計需要很多物件,很多相似又不同的物件,有物件才好辦事嘛。

相似的地方抽離出來就成了抽象物件,比如你要找個身高多少、體重多少、長相如何......的異性。那每個人的標準就不一樣,這些標準就成了物件的特殊性,而相同的就是都是某個人的異性朋友

我們試著來實現一下

function sexFriend(height, weight,appearance){
    var o = new Object();
    o.height=height;
    o.weight=weight;
    o.appearance=appearance;
    o.saySth=function(){
        alert('我願意')
    }
    return o
}
var friend1 = new sexFriend(169, 90, '沉魚落雁,閉月羞花');
var firend2 = new sexFriend(182, 150, '清秀俊朗,風度翩翩');
    
複製程式碼

你看我們能通過給這個函式傳入我們期望的引數就能找到對的那個人吧。

這就是設計模式之——工廠模式,因為JS語言沒有其他語言中的類的概念, 所以用函式來封裝以特定介面建立物件的細節。

但是這種工廠模式僅僅解決了建立多個相似物件的問題,但卻沒有解決物件識別的問題,即沒有辦法僅僅通過這些物件知道他們屬於什麼型別的物件,便是這些物件和構建他們的函式缺少了關聯,就比如說有個人說上面建立的物件是豬的時候,你拿什麼證據去反駁他呢?

隨著JS的發展,建構函式模式應運而生 建構函式模式就能夠解決工廠模式所帶來的問題,因為建構函式建立的物件與建立的函式之間建立了直接的聯絡。通過constructor

function SexFriend(height, weight, appearance){
    this.height=height;
    this.weight=weight;
    this.appearance=appearance;
    this.saySth=function(){
        alert('我願意')
    }
}
var friend1 = new SexFriend(168, 90, '傾國傾城');
var friend2 = new SexFriend(182, 150, '風流倜儻');
複製程式碼

仔細比較上面的建構函式與工廠函式,他們有很多程式碼都是相似的,以下是他們的不同:

  1. 沒有顯式地建立物件
  2. 直接將屬性和方法賦給了 this 物件
  3. 沒有 return 語句
  4. 按照慣例建構函式名字首字母大寫(雖然小寫並不會有不同,為了與普通函式做區分)
  5. 建構函式建立的物件friend1、friend2都有一個constructor的屬性
    friend1.constructor == SexFriend; // true
    friend2.constructor == SexFriend; // true
    複製程式碼

這就使得建立的物件通過constructor屬性找到了標識他的物件型別

建構函式的不足之處

僅僅通過建構函式建立的物件例項是不夠的,每建立一個例項物件,屬性和方法都會隔離存在,無法共用,從而導致資源的浪費。我們來看一種解決方案

function SexFriend(height, weight, appearance){
    this.height=height;
    this.weight=weight;
    this.appearance=appearance;
    this.saySth=saySth;
}
function saySth=function(){
    alert('我願意')
}
var friend1 = new SexFriend(168, 90, '傾國傾城');
var friend2 = new SexFriend(182, 150, '風流倜儻');
複製程式碼

上面的栗子中,把saySth方法放到全域性作用域就能實現共用,如果物件需要定義許多方法,那麼就要定義很多的全域性函式,使得這些自定義的引用型別絲毫沒有封裝可言。

原型模式就是解決上面的問題的

原型模式

我們建立的每一個函式都有一個prototype(原型)屬性,這個屬性是一個指標指向一個物件(瞭解這個很重要

我們在瀏覽器列印上面的建構函式SexFriend的prototype出來看 它是這樣的

console.log(SexFriend.prototype);
/*
{
    constructor: ƒ SexFirend(height, weight, appearance),
    __proto__: Object
}
*/
複製程式碼

這個物件的用途就是由其建構函式建立的所有物件例項都可以共用這個原型物件的屬性。 這個物件的constructor 屬性 就是指向的建構函式本身

SexFriend.prototype.constructor === SexFriend // true
複製程式碼

想想前面我們說建構函式模式的時候,建立的每個例項都有一個 constructor 屬性,事實上並不是例項本身的屬性,而是共用了原型物件上的 constructor 屬性。

在chrome 瀏覽器去列印前面建構函式建立的例項物件frend1和friend2就會知道,這兩個物件除了擁有在建構函式內新增的屬性和方法之外,還有一個屬性 proto 這個屬性也是一個指標,就是指向原型物件的,因此這兩個例項物件都能訪問到共同的 constructor 屬性。

所以當我們需要共用某些方法和屬性的時候就可以利用原型模式將方法屬性繫結到原型物件上

來看具體實現:

function SexFriend(height, weight, appearance){
    this.height=height;
    this.weight=weight;
    this.appearance=appearance;
    // 將共用的方法繫結到原型物件裡
    SexFriend.prototype.saySth=function(){
    alert('我願意')
    }
}
var friend1 = new SexFriend(168, 90, '傾國傾城');
var friend2 = new SexFriend(182, 150, '風流倜儻');
複製程式碼
總結上面所說,要隔離的屬性方法就在建構函式內建立,要完全共用的方法屬性就通過原型物件。

原型鏈

我們在前面說到的原型物件本質上就是一個普通的物件,只不過這個物件與建構函式之間通過 constructor 屬性建立了聯絡。

假如我們將建構函式的 prototype 的指標指向另外一個建構函式的例項物件,如下:

function A(){
    this.a = 1;
}
function B(){
    this.b=2;
}
var b = new B();
A.prototype = b

var a = new A()
a.constructor === A // false

a.__proto__ === b // true
a.constructor === b.constructor // true
b.constructor=== b.__proto__.constructor; // true
b.__proto__.constructor=== B.prototype.constructor; // true
B.prototype.constructor=== B; // true
複製程式碼

我們把建構函式 A 的 prototype 屬性的指標指向了建構函式 B 的例項物件 b; 那麼再通過 A 建立的 a 的__proto__屬性指向的就是 b 了,即原型物件的指標指向了 b ,這時 a 與 A 之間的連線就斷了,但是 A 原本的Prototype(原型)物件仍然存在,只是指標不再指向它了,

並且 a.constructor 已經指向了B

如果再將 B prototype 屬性指標指向建構函式 C 的例項 c , 那麼就有 a.constuctor === C了,由__proto__構成連線的常常的鏈子就是原型鏈了,使得 a.constuctor 找到最後的 C 。也使得例項物件 a 能沿著原型鏈繼承到所有鏈上的方法和屬性,從最近的原型到最遠的原型直到找到相應的屬性方法為止。而最頂級的原型物件就是Object

這就是我所理解的原型鏈,希望對你有所幫助。

感恩紅寶書。

相關文章