物件原型
原型是 JavaScript 物件彼此繼承特性的機制。本文將解釋什麼是原型、原型鏈是如何工作的以及如何設定物件的原型。
原型鏈
在瀏覽器的控制檯中,嘗試建立一個物件字面量
const myObject = {
city: "Madrid",
greet() {
console.log(`Greetings from ${this.city}`);
},
};
myObject.greet(); // Greetings from Madrid
這是一個包含一個數據屬性 city 和一個方法 greet() 的物件。如果你在控制檯中輸入物件的名稱,後面跟著一個點,比如 myObject.,那麼控制檯將彈出一個包含此物件所有可用屬性的列表。你會看到除了 city 和 greet 之外,還有很多其他屬性!
__defineGetter__ __defineSetter__ __lookupGetter__ __lookupSetter__ __proto__ city constructor greet hasOwnProperty isPrototypeOf propertyIsEnumerable toLocaleString toString valueOf
嘗試訪問其中一個
myObject.toString(); // "[object Object]"
它起作用了(即使不清楚 toString() 是做什麼的)。
這些額外的屬性是什麼,它們來自哪裡?
JavaScript 中的每個物件都具有一個內建屬性,稱為其 **原型**。原型本身是一個物件,因此原型將有自己的原型,從而形成所謂的 **原型鏈**。當我們到達原型鏈的末尾,即原型自身的原型為 null 時,鏈條就會結束。
**注意:** 指向物件原型的物件屬性**不**叫 prototype。它的名稱沒有標準,但實際上所有瀏覽器都使用 __proto__。訪問物件原型的標準方法是 Object.getPrototypeOf() 方法。
當你嘗試訪問物件的屬性時:如果在物件本身中找不到屬性,則會在原型中搜索該屬性。如果仍然找不到屬性,則會在原型的原型中搜索,依此類推,直到找到該屬性,或者到達鏈條的末尾,在這種情況下將返回 undefined。
所以當我們呼叫 myObject.toString() 時,瀏覽器
- 在
myObject中查詢toString - 在那裡找不到,因此在
myObject的原型物件中查詢toString - 在那裡找到它並呼叫它。
myObject 的原型是什麼?為了找到答案,我們可以使用 Object.getPrototypeOf() 函式
Object.getPrototypeOf(myObject); // Object { }
這是一個名為 Object.prototype 的物件,它是所有物件預設具有的最基本原型。Object.prototype 的原型是 null,因此它位於原型鏈的末尾
物件的原型並不總是 Object.prototype。試試這個
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。
事實上,當你呼叫熟悉的方法,比如 myDate2.getTime() 時,你是在呼叫 Date.prototype 上定義的方法。
遮蔽屬性
如果你在物件中定義了一個屬性,而該屬性的名稱在物件的原型中也定義了,會發生什麼情況?讓我們看看
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() 方法建立一個新物件,並允許你指定一個用作新物件原型的物件。
以下是一個示例
const personPrototype = {
greet() {
console.log("hello!");
},
};
const carl = Object.create(personPrototype);
carl.greet(); // hello!
這裡我們建立了一個物件 personPrototype,它有一個 greet() 方法。然後我們使用 Object.create() 建立一個新的物件,其原型為 personPrototype。現在,我們可以對新物件呼叫 greet(),原型將提供其實現。
使用建構函式
在 JavaScript 中,所有函式都具有一個名為 prototype 的屬性。當你將函式作為建構函式呼叫時,此屬性將設定為新構造物件的原型(按照慣例,在名為 __proto__ 的屬性中)。
因此,如果我們設定了建構函式的 prototype,我們可以確保使用該建構函式建立的所有物件都將獲得該原型
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.assign 將 personPrototype 中定義的方法放到 Person 函式的 prototype 屬性上。
在這段程式碼之後,使用 Person() 建立的物件將獲得 Person.prototype 作為其原型,該原型自動包含 greet 方法。
const reuben = new Person("Reuben");
reuben.greet(); // hello, my name is Reuben!
這也解釋了為什麼我們之前說 myDate 的原型叫做 Date.prototype:它是 Date 建構函式的 prototype 屬性。
自身屬性
我們使用上面的 Person 建構函式建立的物件有兩個屬性
- 一個
name屬性,它在建構函式中設定,因此它直接出現在Person物件上 - 一個
greet()方法,它在原型中設定。
這種模式很常見,其中方法定義在原型上,但資料屬性在建構函式中定義。這是因為方法通常對我們建立的每個物件都是相同的,而我們通常希望每個物件都具有其資料屬性的自身值(就像這裡每個人的名稱都不同一樣)。
直接在物件中定義的屬性,例如這裡的 name,被稱為 **自身屬性**,你可以使用靜態 Object.hasOwn() 方法檢查屬性是否為自身屬性
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 中,如果 Professor 和 Student 物件可以具有 Person 原型,那麼它們可以繼承公共屬性,同時新增和重新定義需要不同的那些屬性。
在下一篇文章中,我們將討論繼承以及面向物件程式語言的其他主要特性,並瞭解 JavaScript 如何支援它們。