iT邦幫忙

2022 iThome 鐵人賽

DAY 13
2

前言

這篇要介紹的是 JS 的繼承方式: 原型繼承,另外也會介紹幾個重要的專有名詞,包括原型鏈、[[Prototype]] vs __proto__ 等。


繼承和原型繼承

為什麼需要繼承?

透過繼承,我們可以重複使用或延伸原有的程式碼。另一方面,也建立了不同物件的階層關係,可以容易的去模擬真實世界物體的關係。

那原型繼承到底是什麼?

若一個 A 物件可以取用另一個 B 物件的屬性或方法,則該物件 A 繼承 B 物件,而被繼承的物件稱為該繼承物件的原型,而 JS 使用的是用原型去做繼承,和其他語言如 Java 不同,所以以下簡單和類別方式的繼承做比較:

類別繼承(Classical Inheritance):

  1. 子類別繼承自父類別,並透過 new 從類別產生實體物件
  2. 有明顯的層級和分類

原型繼承(Prototypal inheritance):

  1. 直接從物件繼承,被繼承的物件稱為該繼承物件的原型
  2. 物件可以繼承自多個原型

從範例了解原型繼承

以下用一個例子去解釋原型繼承以及原型鏈,讀者可以先閱讀以下程式碼:

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 函式的一些資訊,並從這些資訊來一一推論。

第一階段

  1. 印出 Person 函式的 prototype,會發現是一個原型物件,而那個原型物件的 constructor 為 Person 函式。

  1. 接著來看 Person 函式的 constructor,是 Function 物件,而且巧妙的是 Function 物件的 constructor 就是 Function 物件它自己。
  2. 並且 Function 物件也和第一點 Person 函式的規律一樣,Function 物件的 prototype 為一個原型物件,而那個原型物件的 constructor 為 Function 物件。

  1. 印出 Person.__proto__ 時發現是一串原始碼,用 native code 表示,但在 JS 背後運作中, __proto__ 這個屬性是用來查找一個物件是從哪個原型物件繼承而來,就以這個範例來說是 Function 物件的 prototype 物件。

這樣文字說明看完可能還是霧煞煞,所以來把上面內容整理成一張圖,可以看著圖片搭配文字就更容易懂了。

閱讀小提醒: 可以開雙螢幕一邊放圖一邊放文章說明對照看,不用頻繁上下滾動文章會更好閱讀喔~

消化到這邊後,再來進一步延伸這張圖。

第二階段

  1. 由下面的圖可以知道 Function 原型物件的 __proto__ 就等於 Object 原型物件。

  1. Object.prototype.__proto__ 印出會發現是 null。
  2. 此外,Person 原型物件也是 Object 原型物件。

  1. 透過 Person 函式產生的 user1 物件其 __proto__ 為 Person 函式的原型物件,而建構子為 Person 函式。

到這個階段結束,圖片變成這樣:

分析與總結

根據上面這張圖來整理幾個重點:

  1. 每個物件都有一個 __proto__ 屬性,指向它繼承而來的 prototype 物件,並且每個物件也會有 constructor。
  2. Person 函式、Function 物件、Object 物件中的 prototype 屬性,會指向該物件的原型物件,而那個原型物件的 constructor 為又會指回該物件。
  3. 前面提到原型繼承就是指產生的一個新物件會直接從物件繼承,被繼承的物件稱為該繼承物件的原型,從圖片可以發現 __proto__ 剛好將有繼承關係的物件都串連起來,這個就是原型鏈,將會在後面段落詳細說明。

原型鏈(Prototype Chain)

每個物件都有自己的 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

如果只是想查找一個物件本身有沒有某種屬性,不需要原型鏈查找的功能的話,可以使用 hasOwnProperty 這個函式去查找。

ex:

o = new Object();
o.prop = 'exists';
o.hasOwnProperty('prop');             // 回傳 true
o.hasOwnProperty('toString');         // 回傳 false
o.hasOwnProperty('hasOwnProperty');   // 回傳 false

[[Prototype]] vs __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.

2022/9/25 更新

這裡良葛格有做些補充,詳細可看留言區,我這裡也調整更精確的說法給讀者。
可以得知 __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

Object.prototype.proto

原型基礎物件導向

《JavaScript》重學JS-細聊一下prototype、__proto__與constructor(超詳解版)


上一篇
Day12-介紹 Currying、Partial Application
下一篇
Day14-ES6 Class 繼承
系列文
強化 JavaScript 之 - 程式語感是可以磨練成就的30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中
2
雷N
iT邦研究生 1 級 ‧ 2022-09-13 09:16:22

以前在研究JS怎模擬物件導向的繼承跟組合時, 也有研究__proto__跟prototype chain
讚讚

1
json_liang
iT邦研究生 4 級 ‧ 2022-09-13 10:19:23

這篇真的把原型鏈概念講解的很清晰!感謝大大分享/images/emoticon/emoticon08.gif

1
zhuyueran
iT邦新手 4 級 ‧ 2022-09-13 23:38:22

超贊的/images/emoticon/emoticon12.gif

1
良葛格
iT邦新手 2 級 ‧ 2022-09-25 11:00:33

可以得知 __proto__ 雖然很多瀏覽器都還支援它,但它已經不被 ECMAScript 標準推薦使用

正確來說,__proto__ 因為很多瀏覽器都支援它,ECMAScript 2015 基於歷史相容性,已將之列入標準,不過建議要存取原型物件時,還是透過 Object.getPrototypeOf、Object.setPrototypeOf 等 API,比較正式,畢竟 __xxx__ 名稱上就暗示著別亂碰的概念。

ps. [[Prototype]] 只是規格書上用來表示原型的一個名稱。

harry xie iT邦研究生 1 級 ‧ 2022-09-25 13:25:29 檢舉

沒想到良葛格真的跑來看了XD,謝謝良葛格的補充

我要留言

立即登入留言