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

延伸閱讀

資料來源

0 意見:

張貼留言