2016年6月20日 星期一

[筆記] 了解JavaScript中原型(prototype)、原型鍊(prototype chain)和繼承(inheritance)的概念

img
這篇筆記主要說明了 JavaScript 中非常重要的概念,也就是繼承(inheritance)、原型(prototype)和原型鍊(prototype chain)。

談繼承(inheritance)

讓我們先來了解一下繼承的意思,繼承的意思其實不用想得太複雜,簡單來說就是指一個物件可以提取到其他物件中的屬性(property)或方法(method)
繼承可以分成兩種,一種是 classical inheritance,這種方式用在 C# 或 JAVA 當中;另一種則是 JavaScript 所使用的,是屬於 prototypal inheritance

談原型鍊(prototype chain)

由於 JavaScript 使用的是 prototypal inheritance,所以必然會包含原型(prototype)的概念,讓我們看一下這張圖:
img
一個物件裡面除了所給予的屬性值外,另外也包含原型prototype。
obj.prop1:假設我們現在有一個物件,就稱作 obj ,而這個物件包含一個屬性(property),我們稱作prop1,現在我們可以使用 obj.prop1 來讀取這個屬性的值,就可以直接讀取到 prop1的屬性值了。
obj.prop2:從之前的筆記 ([筆記] 了解function borrowing和function currying ─ bind(), call(), apply() 的應用)我們可以知道,JavaScript 中會有一些預設的屬性和方法,所有的物件和函式都包含 prototype 這個屬性,假設我們把 prototype 叫做 proto,這時候如果我們使用 obj.prop2 的時候,JavaScript 引擎會先在 obj 這個物件的屬性裡去尋找有沒有叫作 prop2 的屬性,如果它找不到,這時候它就會再進一步往該物件的 proto 裡面去尋找。所以,雖然我們輸入 obj.prop2 的時候會得到回傳值,但實際上這不是 obj 裡面直接的屬性名稱,而是在 obj 的 proto 裡面找到的屬性名稱( 即,obj.proto.prop2,但我們不需要這樣打)。
obj.prop3:同樣地,每一個物件裡面都包含一個 prototype,包括物件 proto 本身 也不例外,所以,如果輸入 obj.prop3 時,JavaScript 會先在 obj 這個物件裡去尋找有沒有 prop3 這個屬性名稱,找不到時會再往 objproto 去尋找,如果還是找不到時,就在往 proto 這個物件裡面的 proto 找下去,最後找到後回傳屬性值給我們(obj.proto.proto.prop3)。
雖然乍看之下,prop3 很像是在物件 obj 裡面的屬性,但實在上它是在 obj → prop → prop 的物件裡面,而這樣從物件本身往 proto 尋找下去的鍊我們就稱作原型鍊(prototype chain)。這樣一直往下找會找到什麼時候呢?它會直到某個對象的原型為 null 為止(也就是不再有原型指向)

讓我們來看個例子幫助了解

讓我們實際來看個例子幫助我們了解 prototype chain 這個概念,但是**要注意!要注意!要注意!**這個例子只是為了用來說明 prototype chain 的概念,實際上撰寫程式時萬萬不可使用這樣的方式!
首先,我們先建立一個物件 person 和一個物件 john:
var person = {
  firstname: 'Default',
  lastname: 'Default',
  getFullName: function() {
    return this.firstname + ' ' + this.lastname;
  },
};

var john = {
  firstname: 'John',
  lastname: 'Doe',
};
再次提醒,下面的示範只是為了說明原型鍊,在平常的情況下絕對不要這樣做,這樣做會拖慢整個瀏覽器的效能。
接著,我們知道所有的物件裡面都會包含原型(prototype)這個物件,在 JavaScript 中這個物件的名稱為 __proto__。如同上述原型鍊(prototype chain)的概念,如果在原本的物件中找不到指定的屬性名稱或方法時,就會進一步到 __proto__ 這裡面來找。
為了示範,我們來對 __proto__ 做一些事:
//千萬不要照著下面這樣做,這麼做只是為了示範
john.__proto__ = person;
如此,john 這個物件就繼承了 person 物件。在這種情況下,如果我們想要呼叫某個屬性或方法,但在原本 john 這個物件中找不到這個屬性名稱或方法時,JavaScript 引擎就會到 __proto__ 裡面去找,所以當接著執行如下的程式碼時,並不會噴錯:
console.log(john.getFullName())        //    John Doe;
我們可以得到 "John Doe" 的結果。原本在 john 的這個物件中,是沒有 getFullName() 這個方法的,但由於我讓 __proto__ 裡面繼承了 person 這個物件,所以當 JavaScript 引擎在 john 物件裡面找不到getFullName() 這個方法時,它便會到 __proto__ 裡面去找,最後它找到了,於是它回傳 "John Doe"的結果。
如果我是執行:
console.log(john.firstname);        //  John
我們會得到的是 John 而不是 'Default',因為 JavaScript 引擎在尋找 john.firstname 這個屬性時,在john 這個物件裡就可以找到了,因此它不會在往 __proto__ 裡面找。這也就是剛剛在上面所的原型鍊(prototype chain)的概念, 一旦它在上層的部分找到該屬性或方法時,就不會在往下層的prototype去尋找
在了解了prototype chain這樣的概念後,讓我們接著看下面這段程式碼:
var jane ={
    firstname: 'Jane'
}

jane.__proto__ = person;
console.log(jane.getFullName());
現在,你可以理解到會輸出什麼結果嗎?
答案是 "Jane Default" 。
因為在 jane 這個物件裡只有 firstname 這個屬性,所以當 JavaScript 引擎要尋找 getFullName() 這個方法和 lastname 這個屬性時,它都會去找 __proto__ 裡面,而這裡面找到的就是一開始建立的person 這個物件的內容。

程式範例

var person = {
    firstname:'Default',
    lastname:'Default',
    getFullName: function(){
        return this.firstname+ ' ' + this.lastname;
    }
}


var john = {
    firstname:'John',
    lastname:'Doe'
}

//千萬不要照著下面這樣做,這麼做只是為了示範
john.__proto__ = person;
console.log(john.getFullName());    //  John Doe
console.log(john.firstname);        //  John

var jane ={
    firstname: 'Jane'
}

jane.__proto__ = person;
console.log(jane.getFullName());

參考資料來源

Share:

0 意見:

張貼留言