這篇要介紹的是 JS 的繼承方式: 原型繼承,另外也會介紹幾個重要的專有名詞,包括原型鏈、[[Prototype]] vs __proto__
等。
透過繼承,我們可以重複使用或延伸原有的程式碼。另一方面,也建立了不同物件的階層關係,可以容易的去模擬真實世界物體的關係。
若一個 A 物件可以取用另一個 B 物件的屬性或方法,則該物件 A 繼承 B 物件,而被繼承的物件稱為該繼承物件的原型,而 JS 使用的是用原型去做繼承,和其他語言如 Java 不同,所以以下簡單和類別方式的繼承做比較:
以下用一個例子去解釋原型繼承以及原型鏈,讀者可以先閱讀以下程式碼:
function Person(firstName, lastName) {
this.firstName = firstName,
this.lastName = lastName,
this.fullName = function() {
return `${this.firstName} ${this.lastName}`;
}
}
const user1 = new Person("Harry", "Xie");
console.log(user1);
我們把這段程式碼丟到開發人員工具去,然後來印出 Person 函式的一些資訊,並從這些資訊來一一推論。
Person.__proto__
時發現是一串原始碼,用 native code 表示,但在 JS 背後運作中, __proto__
這個屬性是用來查找一個物件是從哪個原型物件繼承而來,就以這個範例來說是 Function 物件的 prototype 物件。這樣文字說明看完可能還是霧煞煞,所以來把上面內容整理成一張圖,可以看著圖片搭配文字就更容易懂了。
閱讀小提醒: 可以開雙螢幕一邊放圖一邊放文章說明對照看,不用頻繁上下滾動文章會更好閱讀喔~
消化到這邊後,再來進一步延伸這張圖。
__proto__
就等於 Object 原型物件。Object.prototype.__proto__
印出會發現是 null。__proto__
為 Person 函式的原型物件,而建構子為 Person 函式。到這個階段結束,圖片變成這樣:
根據上面這張圖來整理幾個重點:
__proto__
屬性,指向它繼承而來的 prototype 物件,並且每個物件也會有 constructor。__proto__
剛好將有繼承關係的物件都串連起來,這個就是原型鏈,將會在後面段落詳細說明。每個物件都有自己的 prototype 物件,層層往上直到最後一個物件的原型為 null,而每個 prototype 物件一層層串起就為原型鏈。
圖中,由紅色箭頭 proto
指向連接起來的結構,就是原型鏈。
當尋找一個物件的屬性,如果沒找到會根據原型鏈上找,直到找到給定名稱的屬性為止。但到達原型鏈的頂部時 - 也就是 Object.prototype ,仍然沒有找到指定的屬性的話就會返回 undefined。
因此透過 prototype 屬性和原型鏈,能讓構造函式產生的實體之間可以共享 prototype 物件的屬性和方法。
假如沒有原型鏈這樣共享物件的屬性和方法的設計,當透過**工廠函式(factory function)**的方式建立物件,那每個函式都會在物件上建立一次。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
console.log(this.name);
};
}
const person1 = new Person("Stone", 28, "Software Engineer");
const person2 = new Person("Sophie", 29, "English Teacher");
如果把 sayName 抽成全域函式呢? 這樣就要有更多的全域函式要管理,而且任何人都可以取用它,並且有些情況下,就是特定物件才可以去取用的。所以才要透過原型繼承的方式,達到讓物件的函式共用+封裝的效果。
例如以下程式碼,sayName 是兩個物件共享的,但它們兩個物件又有各自的屬性。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Jack", "Tom"];
}
Person.prototype = {
constructor : Person,
sayName : function(){
console.log(this.name);
}
}
const person1 = new Person("Stone", 28, "Software Engineer");
const person2 = new Person("Sophie", 29, "English Teacher");
person1.friends.push("Jenny");
console.log(person1.friends); // ['Jack', 'Tom', 'Jenny']
console.log(person2.friends); // ['Jack', 'Tom']
console.log(person1.friends === person2.friends); // false
console.log(person1.sayName === person2.sayName); // true
如果只是想查找一個物件本身有沒有某種屬性,不需要原型鏈查找的功能的話,可以使用 hasOwnProperty 這個函式去查找。
ex:
o = new Object();
o.prop = 'exists';
o.hasOwnProperty('prop'); // 回傳 true
o.hasOwnProperty('toString'); // 回傳 false
o.hasOwnProperty('hasOwnProperty'); // 回傳 false
__proto__
最後這邊要介紹一下 [[Prototype]] 和 __proto__
這兩個屬性和它們的差異。
在前面的範例中,console.log(user)
後看到的是 [[Prototype]],而不是 __proto__
,這是怎麼回事呢?
我們來看看根據 MDN Inheritance and the prototype chain 的文件的說明:
Note: Following the ECMAScript standard, the notation someObject.[[Prototype]] is used to designate the prototype of someObject. Since ECMAScript 2015, the [[Prototype]] is accessed using the accessors Object.getPrototypeOf() and Object.setPrototypeOf(). This is equivalent to the JavaScript property proto which is non-standard but de-facto implemented by many browsers.
以及 MDN Object.prototype.proto 中的描述。
Warning: While Object.prototype.proto is supported today in most browsers, its existence and exact behavior has only been standardized in the ECMAScript 2015 specification as a legacy feature to ensure compatibility for web browsers. For better support, use Object.getPrototypeOf() instead.
這裡良葛格有做些補充,詳細可看留言區,我這裡也調整更精確的說法給讀者。
可以得知 __proto__
被 ECMAScript 2015 納入標準,不過是 legacy feature,推薦使 Object.getPrototypeOf()
與 Object.setPrototypeOf()
這兩個 accessors 取得和設定原型鏈的屬性。
原舊文: 可以得知
__proto__
雖然很多瀏覽器都還支援它,但它已經不被 ECMAScript 標準推薦使用,取而代之的是[[Prototype]]
,[[Prototype]]
使用Object.getPrototypeOf()
與Object.setPrototypeOf()
這兩個 accessors 取得和設定原型鏈的屬性。
例如要檢查一個物件 user1 有沒有特定屬性 prop,如果物件本身沒有,就檢查 Object.getPrototypeOf(user1).prop
,再沒有就檢查 Object.getPrototypeOf(Object.getPrototypeOf(user1)).prop
,依此類推,都找不到則返回 undefined。
這篇就介紹到這裡了,下篇開始也會介紹一些和繼承相關的東西,敬請期待!
Inheritance and the prototype chain
《JavaScript》重學JS-細聊一下prototype、__proto__與constructor(超詳解版)
可以得知 __proto__ 雖然很多瀏覽器都還支援它,但它已經不被 ECMAScript 標準推薦使用
正確來說,__proto__ 因為很多瀏覽器都支援它,ECMAScript 2015 基於歷史相容性,已將之列入標準,不過建議要存取原型物件時,還是透過 Object.getPrototypeOf、Object.setPrototypeOf 等 API,比較正式,畢竟 __xxx__ 名稱上就暗示著別亂碰的概念。
ps. [[Prototype]] 只是規格書上用來表示原型的一個名稱。
沒想到良葛格真的跑來看了XD,謝謝良葛格的補充