繼承機制
要來了解 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 的 instancepeter 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 物件導向的應用上更能顯現出它的功效,物件導向的部分會在其他文章介紹到。