[ 筆記 ] JavaScript 進階 07 - Prototype Chain


Posted by krebikshaw on 2020-09-27

繼承機制

要來了解 Prototype 之前,要先了解什麼是「繼承機制」,以及為什麼要有「繼承機制」。

繼承機制可以把它想像成「傳承」的概念,就像人類的發展需要靠知識的傳承一樣。人類之所以可以傳承知識,就是因為有前人把知識給「紀錄」下來,讓後人可以透過這些「紀錄」快速的學習到知識。如果少了這些「紀錄」,每個人出生下來就只能靠著自己個人的體會來認識這個世界,發展就會非常緩慢。

高中大家在學習的時候,都會有自己做筆記的方式,雖然每個人的筆記都不相同,但都是「繼承」於老師所傳遞的知識,而老師們的知識,又會是「繼承」於教育部所制定的課綱

如果可以把「資源」有效的繼承下來,那每次要利用這個資源的時候,就只需要把要處理的事情,都指派給同一個資源,讓它幫你處理。

如果教室前面有三個神奇寶箱(國文寶箱、英文寶箱、數學寶箱),你只要把題目放入對的寶箱當中,他就會幫你把答案寫好並還給你。請問你手上有一堆題目要寫,你會選擇把題目放入對應的寶箱,還是會自己親手造一個寶箱?

這個問題有點極端,但是我想表達的意思就是,要如何有效率的去應用你所擁有的「資源」。假如課本都已經告訴你可以利用畢氏定理來求出三角形的邊長,你就只需要把手上的題目「指向」解題的公式,沒必要自己開始從頭推導數學公式。

換句話說:
哪天你發現自己的筆記有地方漏寫了,你知道要去老師那邊找,因為你的筆記是從老師那邊「繼承」的,所以老師如果也沒有答案的話,就接著去教育部的課綱裡面找,因為老師的知識也是「繼承」於教育部課綱的。

JavaScript 的繼承機制

JavaScript 不像其他程式語言有 Class 的機制,JavaScript 用了不同的方式實作出「繼承」的功能,而這個功能讓我們可以在不同的物件之間,共享物件的屬性及方法(property & method)

舉例來說:

function Person(name) {
  this.name = name;
  this.getName = function(){
    return this.name;
  }
}

const nick = new Person('nick');
const peter = new Person('peter');
console.log(nick.getName === peter.getName); // => false

儘管兩個 instance 都是回傳 function(){ return this.name; } 內容,但因為 instance 的記憶體位置不同,會被視為不同的方法。

一但 instance 越來越多,需要建立的方法也會越來越多,可能會造成記憶體空間的浪費,為了要貫徹「共享」的概念,我們希望底下的 instance 可以一起使用相同的 getName 方法

Prototype 共用方法

當我們建立好一個資源,並且想把它的方法提供給大家共享時,可以把這個方法建立在 prototype 底下

  • Person.prototype.getName
function Person(name) {
  this.name = name
}

// 想要提供給大家的方法,建立在 prototype 底下
Person.prototype.getName = function() {
  return this.name;
}

只要是「繼承」這個資源的物件,就可以共享 prototype 提供的方法

  • Peter.getName
const peter = new Person('peter');
console.log(Peter.getName)  // 這邊直接用了 Person.prototype 提供的 getName

所以就算有更多的 instance,也都會共享同一個方法

function Person(name) {
  this.name = name;
}
Person.prototype.getName = function() {
  return this.name;
}

const nick = new Person('nick');
const peter = new Person('peter');
console.log(nick.getName === peter.getName); // => true

因為方法是「共享」的,所以每當要需要使用到方法時,都一定要透過一個「路徑」去尋找到這個方法,才能順利的使用它

  • 就像 Peter.getName 需要知道這個 getName 的方法要去哪裡找,找到了才能使用它
  • 它找到了這個 getName 方法,是由 Person.prototype 提供的

而這個找尋方法的「路徑」,就是所謂的 Prototype Chain 原型鍊

Prototype Chain 原型鍊

當學生「繼承」了老師的知識時,老師只要需要把解題的訣竅放在 prototype 底下,就可以讓學生來共享這份方法,

  • 老師.prototype.畢氏定理

哪天我們發現自己的筆記裡沒寫到這個方法時,就去老師那裡找。這個找尋方法的路徑,就是透過 __proto__ 來實現的。

  • 學生.__proto__ 可以找到老師那裡提供的方法
  • 學生.__proto__ === 老師.prototype 就可以找到畢氏定理

如果連老師那裡都沒有找到這個方法時,就再到教育部課綱去找

  • 學生.__proto__.__proto__ 可以找到教育部課綱裡提供的方法
  • 學生.__proto__.__proto__ === 教育部課綱.prototype

在使用 new 的同時,instance (peter) 會自動加上 .proto,並且指向建構函式 (Person) 的 .prototype

回到這段程式碼:

function Person(name) {
  this.name = name;
}
Person.prototype.getName = function() {
  return this.name;
}

當我們做了 new 的動作時:

  • const nick = new Person('nick');
  • const peter = new Person('peter');
peter.__proto__ 就會指向 => Person.prototype 
nick.__proto__ 就會指向 => Person.prototype

而建構函式 Person 其實也是 Object new 出來的啊,所以 Person.prototype 也是會有 __proto__ 同樣指向 Object.prototype

1. Person 是 function 的 instance
    => 可以想像成是 var Person = new Function();
    => Person.__proto__ === Function.prototype
2. function 是 Object 的 instance
    => 可以想像成是 var Function = new Object();
    => Function.prototype.__proto === Object.prototype
    => Person.__proto__.__proto__ === Object.prototype

所以可以看得出來, peter (instance) 的原型會指向 Person (constructor),而 Person (constructor) 的原型又會指向 Object,這樣鍊型的關係達到了繼承的效果,而又稱為原型鍊。

Prototype Chain : 尋找的順序

console.log(nick.getName());

所以要怎麼找到 getName 這個 method 呢?

1. nick
2. nick.__proto__
   ( = Person.prototype )
3. nick.__proto__.__proto__
   ( = Person.prototype.__proto__ )
   ( = Object.prototype )
4. nick.__proto__.__proto__.__proto__ => 最上層,也就是 null
   ( = Person.prototype.__proto__.__proto__ )
   ( = Object.prototype.__proto__ )

所以 JS 會一直往上找,一但找到該 method 就會停止。
如果 Person 跟 Object 的 prototype 都有 getName,真正回傳的會是 Person的 getName,因為 Person 的尋找優先順序比較高。

所以這樣 一層一層往上查找,就構成一條原型鍊 prototype chain,而這條鍊的最頂層就是 Object.prototype.__proto__ 也就是 null。

注意: .protp 這個指引的屬性在實作上可以省略,這邊為了說明 JS 底層實作的概念所以都有寫出來,實務上盡量不要去寫到 .proto 這個屬性。

可以再看下一個例子,確定自己清楚 prototyp 的尋找順序:

function Person(name) {
  this.name = name;
  this.sayHi = function () {
    console.log(this.name, 'Hi => method of instance'); // => new 出幾份 instance 就會有幾份,佔用記憶體空間
  }
}

Person.prototype.sayHello = function () {
  console.log(this.name, 'Hello => method of Class'); // => 用 prototype 實現共享 property & method
}

Object.prototype.sayHello = function () {
  console.log(this.name, 'Hello => method of Object'); // => method 跟 Person 重複,所以找到 Person 就停了
}

Object.prototype.sayYo = function () {
  console.log(this.name, 'Yo  => method of Object');
}
const peter = new Person('peter');

peter.sayHi(); // peter Hi => method of instance
peter.sayHello(); // peter Hello => method of Class
peter.sayYo(); // peter Yo  => method of Object

// => 這樣才可以使用到 Object 的 method
Object.sayHello.call(peter); // peter Hello => method of Object

好用內建函式

  • hasOwnProperty: 可以確認是否為 instance 自己的方法
    // 如果是自己 instance 的方法就會回傳 true 
     peter.hasOwnProperty(sayHi); // => true
    // 如果不是自己 instance 的方法就會回傳 false
     peter.hasOwnProperty(sayHello); // => false
    
  • instanceof: 確認 A 是否為 B 的 instance
    peter instanceof Person; // true
    Person instanceof Function; // true
    Person instanceof Object; // true
    // Function 跟 Object 互為對方的 intance,好奇妙
    Function instanceof Object; // true 
    Object instanceof Function; // true
    

最後一個概念:

每一個 prototype 都有個 constructor 屬性,而想當然爾就是指向自己(建構函式)囉。

peter.constructor === Person; // true;
Person.prototype.constructor === Person; // true
Person.prototype.hasOwnProperty('constructor'); // true

了解 Prototype Chain 的概念以後,就能理解 JavaScript 是如何實作出「繼承」的功能了。

而這個繼承的功能,在 OOP 物件導向的應用上更能顯現出它的功效,物件導向的部分會在其他文章介紹到。


#Prototype Chain







Related Posts

我很菜,所以只會用原生 JS 跟 CSS 寫「口罩地圖 」Ep.02

我很菜,所以只會用原生 JS 跟 CSS 寫「口罩地圖 」Ep.02

JavaScript 基礎筆記

JavaScript 基礎筆記

一個有趣的 styled components bug

一個有趣的 styled components bug


Comments