JS基本觀念: 原型鏈(prototype chain)

Charles Huang
9 min readOct 19, 2019

之前在寫JS基本觀念:typeof vs instanceof時,有稍微帶到一些原型鍊,不過當初沒特別去講解。最近我回過頭來檢視自己對於原型鏈的理解後發現我也沒說非常的了解原理,所以經過一些survey後有了這篇我對於原型鏈的理解。

前言

原型鍊(prototype chain)在javascript中一直是很重要的一環,不過也讓人挺不好理解的,尤其裡面一堆專有名詞,例如說prototype, __proto__, constructor, Object.prototype, Function.prototype, new等等。

光是看都快昏了! 但偏偏你又避不了原型鏈,他也是面試必考題之一,所以還是來好好面對它吧!

Javascript中的class

以下這篇是很好的切入點,講到了當初開發javascript這語言的工程師當初是怎樣設計prototype的。挺建議先看過的。

Javascript继承机制的设计思想

首先,javascript不像java之類的物件導向語言有class的概念(es6的class只是語法糖),不過他也設計出了類似的繼承機制。

如果你今天是用java的class來產生一個instance的話,你會這樣寫

Person p = new Person();

於是javascript就把 new也引用進來,不過new後面接的是class,但javascript沒有class啊?那我們該接什麼呢?

這時候工程師就想到,如果是以java來看的話,java在new instance時會呼叫class的constructor,也就是構建函數,所以javascript就做了個簡化,就在後面直接接了構建函數而不是class。

所以,看到以下程式碼

// constructor
function Person(name, age) {
this.name = name;
this.age = age;
}
var personA = new Person('Charles', '30');
var personB = new Person('Jane', 29);
console.log(personA.name); // Charles
console.log(personB.name); // Jane

這邊Person就是一個構造函數,表示對象的原型,而javascript利用new + 後面的構造函數就能創造出了一個Person的實例。

new的缺點

延續上面的程式碼,可以很清楚地知道,personA & personB都是獨立的instance,裡面的name & age都是獨立且不互相干擾的。

比如我修改了personAage,此時personBage仍然是29。

personA.age = 20;
console.log(personB.age); // 29

同樣的,若我今天在Person當中新增一個function。

function Person(name, age) {
this.name = name;
this.age = age;
this.log = function () {
console.log(this.name + ', age:' + this.age);
}
}
var personA = new Person('Charles', '30');
var personB = new Person('Jane', 29);
personA.log(); // Charles, age: 30
personB.log(); // Jane, age: 29
console.log(personA.log === personB.log) // false

從上面可以發現log 這個function也是各自獨立的,這就造成了相同功能卻佔據了兩份資源的狀況。

所以單純用new建立出來的instance會有無法共用屬性與資源浪費的狀況。

原型鏈的引入

為了解決上面碰到的共享問題,工程師替構造函數加入了原型鏈(prototype)的屬性。所有要共享的屬性與方法,就會被放在原型鏈內,而想要各自擁有的就放在構造函數內。當instance被建立後,會自動引用對應的原型鏈內所有的屬性與方法。

讓我們來改寫下上面的例子吧

function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.log = function() {
console.log(this.name + ', age:' + this.age);
}
var personA = new Person('Charles', '30');
var personB = new Person('Jane', 29);
console.log(personA.log === personB.log) // true
// 功能依舊
personA.log(); // Charles, age: 30
personB.log(); // Jane, age: 29

OK,我們成功地把log方法抽出來變成共用的了,同時如果你修改log方法,會發現所有instance都一併被影響了。

Person.prototype.log = function() {
console.log('Name: ' + this.name + ', age:' + this.age);
}
personA.log(); // Name: Charles, age: 30
personB.log(); // Name: Jane, age: 29

原型鏈的運作

剛剛上面所說的都是跟你說,嘿,你有一個建構函數並且有prototype可用呢,但你有想過到底是怎樣運作的麼?

以上面的 var personA = new Person(‘Charles’, ‘30’); 為例,呼叫personA.log()時,javascript到底是怎樣找到那個log function的呢?

personA這個instance本身是沒有log這個function的,依照javascript的機制,personA屬於Person的一個實例,所以如果在personA找不到,那我們會去Person.prototype那找。那中間到底是怎樣讓personA連結到Person.prototype呢?

答案是, __proto__ (REF: MDN)

function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.log = function() {
console.log(this.name + ', age:' + this.age);
}
var personA = new Person('Charles', '30');
console.log(personA.__proto__ === Person.prototype) // true

personA__proto__會指到Person.prototype,所以在personA沒找到log function,javascript就會透過__proto__Person.prototype去找。

那假如Person.prototype裡也還是沒有log function呢?那javascript會依照同樣邏輯,往Person.prototype.__proto__去找,依此類推,直到某一次的__proto__指到null為止。

而上面這一條透過__proto__不斷串起來的鍊,就叫做原型鍊。透過這一條原型鍊,就可以達成類似繼承的功能,可以呼叫自己 parent 的 method。

以下以幾個例子來看看原型鏈吧!希望能讓你更明白怎樣一路往上串的。

function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.log = function() {
console.log(this.name + ', age:' + this.age);
}
var personA = new Person('Charles', '30');
console.log(personA.__proto__ === Person.prototype) // true
console.log(Person.prototype.__proto__ === Object.prototype) // true
console.log(Object.prototype.__proto__) // null

如果想知道一個屬性是存在於instance本身,還是存在於原型鏈當中,可以使用hasOwnProperty這個function。

function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.log = function() {
console.log(this.name + ', age:' + this.age);
}
var personA = new Person('Charles', '30');
console.log(personA.hasOwnProperty('log')); // false
console.log(personA.__proto__.hasOwnProperty('log')); // true

END

一路也看了不少文章,感謝這些文章帶給我的成長,我這篇不是什麼新東西或知識了,對我來說也是個學習紀錄,如果有緣能看到這篇,希望也同樣能給你點收穫!

--

--

Charles Huang

Made in Taiwan的後端工程師,擅長nodejs做後端開發。相信分享與交流可以讓世界更美好,加上自己有點金魚腦,所以開始了寫些有的沒的之路XD