物件原型

原型是 JavaScript 物件彼此繼承特性的機制。本文將解釋什麼是原型、原型鏈是如何工作的以及如何設定物件的原型。

先決條件 瞭解 JavaScript 函式,熟悉 JavaScript 基礎知識(參見 第一步構建模組),以及 OOJS 基礎知識(參見 物件簡介)。
目標 瞭解 JavaScript 物件原型、原型鏈的工作原理以及如何設定物件的原型。

原型鏈

在瀏覽器的控制檯中,嘗試建立一個物件字面量

js
const myObject = {
  city: "Madrid",
  greet() {
    console.log(`Greetings from ${this.city}`);
  },
};

myObject.greet(); // Greetings from Madrid

這是一個包含一個數據屬性 city 和一個方法 greet() 的物件。如果你在控制檯中輸入物件的名稱,後面跟著一個點,比如 myObject.,那麼控制檯將彈出一個包含此物件所有可用屬性的列表。你會看到除了 citygreet 之外,還有很多其他屬性!

__defineGetter__
__defineSetter__
__lookupGetter__
__lookupSetter__
__proto__
city
constructor
greet
hasOwnProperty
isPrototypeOf
propertyIsEnumerable
toLocaleString
toString
valueOf

嘗試訪問其中一個

js
myObject.toString(); // "[object Object]"

它起作用了(即使不清楚 toString() 是做什麼的)。

這些額外的屬性是什麼,它們來自哪裡?

JavaScript 中的每個物件都具有一個內建屬性,稱為其 **原型**。原型本身是一個物件,因此原型將有自己的原型,從而形成所謂的 **原型鏈**。當我們到達原型鏈的末尾,即原型自身的原型為 null 時,鏈條就會結束。

**注意:** 指向物件原型的物件屬性**不**叫 prototype。它的名稱沒有標準,但實際上所有瀏覽器都使用 __proto__。訪問物件原型的標準方法是 Object.getPrototypeOf() 方法。

當你嘗試訪問物件的屬性時:如果在物件本身中找不到屬性,則會在原型中搜索該屬性。如果仍然找不到屬性,則會在原型的原型中搜索,依此類推,直到找到該屬性,或者到達鏈條的末尾,在這種情況下將返回 undefined

所以當我們呼叫 myObject.toString() 時,瀏覽器

  • myObject 中查詢 toString
  • 在那裡找不到,因此在 myObject 的原型物件中查詢 toString
  • 在那裡找到它並呼叫它。

myObject 的原型是什麼?為了找到答案,我們可以使用 Object.getPrototypeOf() 函式

js
Object.getPrototypeOf(myObject); // Object { }

這是一個名為 Object.prototype 的物件,它是所有物件預設具有的最基本原型。Object.prototype 的原型是 null,因此它位於原型鏈的末尾

Prototype chain for myObject

物件的原型並不總是 Object.prototype。試試這個

js
const myDate = new Date();
let object = myDate;

do {
  object = Object.getPrototypeOf(object);
  console.log(object);
} while (object);

// Date.prototype
// Object { }
// null

這段程式碼建立了一個 Date 物件,然後沿著原型鏈向上遍歷,記錄原型。它向我們展示了 myDate 的原型是一個 Date.prototype 物件,而物件的原型是 Object.prototype

Prototype chain for myDate

事實上,當你呼叫熟悉的方法,比如 myDate2.getTime() 時,你是在呼叫 Date.prototype 上定義的方法。

遮蔽屬性

如果你在物件中定義了一個屬性,而該屬性的名稱在物件的原型中也定義了,會發生什麼情況?讓我們看看

js
const myDate = new Date(1995, 11, 17);

console.log(myDate.getTime()); // 819129600000

myDate.getTime = function () {
  console.log("something else!");
};

myDate.getTime(); // 'something else!'

鑑於對原型鏈的描述,這應該是可以預料到的。當我們呼叫 getTime() 時,瀏覽器首先在 myDate 中查詢具有該名稱的屬性,並且只有在 myDate 沒有定義它時才檢查原型。因此,當我們在 myDate 中新增 getTime() 時,將呼叫 myDate 中的版本。

這被稱為“遮蔽”屬性。

設定原型

在 JavaScript 中,有各種設定物件原型的方法,這裡將介紹兩種:Object.create() 和建構函式。

使用 Object.create

Object.create() 方法建立一個新物件,並允許你指定一個用作新物件原型的物件。

以下是一個示例

js
const personPrototype = {
  greet() {
    console.log("hello!");
  },
};

const carl = Object.create(personPrototype);
carl.greet(); // hello!

這裡我們建立了一個物件 personPrototype,它有一個 greet() 方法。然後我們使用 Object.create() 建立一個新的物件,其原型為 personPrototype。現在,我們可以對新物件呼叫 greet(),原型將提供其實現。

使用建構函式

在 JavaScript 中,所有函式都具有一個名為 prototype 的屬性。當你將函式作為建構函式呼叫時,此屬性將設定為新構造物件的原型(按照慣例,在名為 __proto__ 的屬性中)。

因此,如果我們設定了建構函式的 prototype,我們可以確保使用該建構函式建立的所有物件都將獲得該原型

js
const personPrototype = {
  greet() {
    console.log(`hello, my name is ${this.name}!`);
  },
};

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

Object.assign(Person.prototype, personPrototype);
// or
// Person.prototype.greet = personPrototype.greet;

這裡我們建立

  • 一個物件 personPrototype,它有一個 greet() 方法
  • 一個 Person() 建構函式,它初始化要建立的人員的名稱。

然後,我們使用 Object.assignpersonPrototype 中定義的方法放到 Person 函式的 prototype 屬性上。

在這段程式碼之後,使用 Person() 建立的物件將獲得 Person.prototype 作為其原型,該原型自動包含 greet 方法。

js
const reuben = new Person("Reuben");
reuben.greet(); // hello, my name is Reuben!

這也解釋了為什麼我們之前說 myDate 的原型叫做 Date.prototype:它是 Date 建構函式的 prototype 屬性。

自身屬性

我們使用上面的 Person 建構函式建立的物件有兩個屬性

  • 一個 name 屬性,它在建構函式中設定,因此它直接出現在 Person 物件上
  • 一個 greet() 方法,它在原型中設定。

這種模式很常見,其中方法定義在原型上,但資料屬性在建構函式中定義。這是因為方法通常對我們建立的每個物件都是相同的,而我們通常希望每個物件都具有其資料屬性的自身值(就像這裡每個人的名稱都不同一樣)。

直接在物件中定義的屬性,例如這裡的 name,被稱為 **自身屬性**,你可以使用靜態 Object.hasOwn() 方法檢查屬性是否為自身屬性

js
const irma = new Person("Irma");

console.log(Object.hasOwn(irma, "name")); // true
console.log(Object.hasOwn(irma, "greet")); // false

**注意:** 你也可以在這裡使用非靜態 Object.hasOwnProperty() 方法,但我們建議你如果可以的話使用 Object.hasOwn()

原型和繼承

原型是 JavaScript 的強大且非常靈活的特性,它使程式碼重用和物件組合成為可能。

特別是,它們支援 **繼承** 的一種版本。繼承是面向物件程式語言的一種特性,它允許程式設計師表達系統中某些物件是其他物件的更專業化版本的理念。

例如,如果我們正在為學校建模,我們可能會有教授學生:他們都是,因此具有某些共同特徵(例如,他們都有名字),但每個人可能會新增額外的特徵(例如,教授有他們教授的科目),或者可能會以不同的方式實現相同的特徵。在 OOP 系統中,我們可能會說教授和學生都**繼承自**人。

你可以看到,在 JavaScript 中,如果 ProfessorStudent 物件可以具有 Person 原型,那麼它們可以繼承公共屬性,同時新增和重新定義需要不同的那些屬性。

在下一篇文章中,我們將討論繼承以及面向物件程式語言的其他主要特性,並瞭解 JavaScript 如何支援它們。

總結

本文涵蓋了 JavaScript 物件原型,包括原型物件鏈如何允許物件彼此繼承特性、原型屬性以及如何使用它向建構函式新增方法,以及其他相關主題。

在下一篇文章中,我們將介紹面向物件程式設計背後的概念。