本文最初發布於 Aparna Joshi 網站,經原作者授權由 InfoQ 中文站翻譯並分享。
使用任何可用方法創建的所有 JavaScript構造函數(constructor function) 都包含一個屬性。這就是 原型 屬性。這裡很重要的是,原型屬性本身就是一個對象。
構造函數的原型屬性可用於訪問 / 修改方法,以及訪問 / 修改在創建對象時分配的原型對象中存在的其他屬性。
每個原型對象都有一個稱為 構造器(constructor) 的屬性。這個屬性指向構造函數 (Constructor Function) 本身。
function Name(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
this.fullName = firstName + " " + lastName
}
var jensen = new Name("Jensen", "Ackles");
console.log(jensen);
如果嘗試訪問包含對象 jensen 的所有屬性的 console.log,我們將得到以下結果。
到這裡發生過的事情原理是這樣的:
Name 是一個構造函數。它包含 原型 屬性。
這個原型屬性有一個稱為 constructor 的屬性,該屬性指向 Name 構造函數本身。可以將其他任何屬性(attribute)添加到這個屬性上。
使用 Name 構造器創建新對象 jensen 後,這個對象可以訪問屬於 Name 函數的所有屬性,包括其原型。
可以使用 proto 對象從新對象 jensen 中訪問 Name 構造函數的 原型。
由於 原型 本身是一個對象,因此它還包含一個 原型 屬性。這樣就創建了 原型鏈:
https://aparnajoshi.netlify.app/javascript-prototype-inner-workings-of-objects#prototype-chain
一些瀏覽器可以支持通過 proto 對象訪問構造函數的原型。我們不建議在 JavaScript 編程中使用這個特性(該特性是非標準特性,可能無法在所有瀏覽器中都正常使用),但也可以用它在開發過程中快速檢查原型鏈的運行狀況。
proto 的替代方法包括 Object.getPrototypeOf() 或 objectInstance.constructor.prototype。對於上述示例來說,可以通過以下方式使用它來訪問相同的原型屬性:Object.getPrototypeOf(jensen);
jensen.constructor.prototype;
我們通常是通過某些構造函數來創建對象的。如果未使用任何用戶定義的構造器,則表示該對象是使用 JavaScript 的 Object Constructor(對象構造器) 創建的。這意味著我們創建的任何對象最終都將從 JavaScript 的對象構造器繼承。
看一下下面創建的對象,看看它們的 proto 對象包含哪些內容。function Fruit() {
this.value = 10;
this.quantity = 35;
}
function Apple(name, color) {
this.name = name;
this.color = color
}
Apple.prototype = new Fruit();
var apple1 = new Apple("Apple", "Red");
console.log(apple1);
如果我們檢查對象 apple1 的內部屬性,則可以觀察到以下內容:
對象 apple1 包含兩個主要屬性:name 和 color。這些屬性的值與在創建過程中分配給它們的值是一樣的。
對象 apple1 的 proto 屬性指向 Fruit 對象的實例。反過來,這又包含另外兩個屬性:value 和 quantity。
如果檢查 Fruit 實例的 proto 屬性,我們會發現它最終指向 JavaScript 的 Object 的原型。
當一個屬性不直接存在於一個對象上時,JavaScript 會沿著 原型鏈 向上尋找,以在其最近的原型中找到該屬性。就像 JavaScript 的作用域鏈一樣,原型鏈也會不斷上升,直到到達 Object.prototype 為止。
原型在 JavaScript 中廣泛用於實現繼承。傳統上,JavaScript 僅用於腳本編寫,並且不需要像其他語言一樣提供面向對象的編程特性。但是,原型的概念可用來將方法和屬性從一個構造函數傳遞到另一個構造函數。
function Fruit() {
this.value = 10;
this.quantity = 35;
}
Fruit.prototype.setValue = function(value) {
this.value = value;
}
function Apple(name, color) {
this.name = name;
this.color = color
}
Apple.prototype = new Fruit();
var apple1 = new Apple("Apple", "Red");
apple1.setValue(20);
console.log(apple1.name); // Apple
console.log(apple1.value); // 20
console.log(apple1.quantity); // 35
在上面的示例中,即使新對象 apple1 不具有 value 和 quantity 屬性,我們仍然可以訪問它們。需要注意的是,添加到 Fruit 構造函數 的原型屬性上的 setValue 方法也可以通過對象 apple1 訪問。這就是在 JavaScript 中實現繼承的方式。
使用任何構造器創建對象時,它都會附帶某些可應用於對象的內置方法。其中一些內置方法包括 hasOwnProperty()、isPrototypeOf()、propertyIsEnumerable()、toLocaleString()、toString() 和 valueOf()。這些內置方法可用於所有對象。這是因為 JavaScript 中的所有對象都從 Object.prototype 繼承屬性和方法。
所有內置的構造器,例如 Array()、Number() 和 String() 等,都是從 JavaScript 的 Object 構造器創建的,並且它們的原型也分配給 Object.prototype。
JavaScript 中的原型有很多用途,它可用於繼承父函數的方法,還可以用於抽象數據層,並僅公開 getter 和 setter 方法來操作屬於各種對象的值。但原型也有其缺點。原型對象上添加的所有屬性,對於使用其 構造函數 創建的對象的每個實例都是通用的。對於其中任一屬性的任何更改都將反映在所有對象中。
function Apple(name, color) {
this.name = name;
this.color = color
}
Apple.prototype.value = 20;
var apple1 = new Apple("Apple", "Red");
var apple2 = new Apple("Apple2", "Wheatish Red");
console.log(apple1.name); // Apple
console.log(apple1.value); // 20
console.log(apple2.value); // 20
Apple.prototype.value = 40;
console.log(apple1.value); // 40
console.log(apple2.value); // 40
apple1.value = 30;
console.log(apple1.value); // 30
console.log(apple2.value); // 40
在上面的示例中,直接在構造器原型上進行的更改會反映在其所有對象中;但是,當更改對象 apple1 內部的屬性 value 時,這一更改就不會反映在其他對象中。這是因為 apple1 現在已經創建了自己的屬性 value,並且從這個實例開始,apple1.value 將始終引用為其自身的屬性 value,而不是繼承的屬性。
為了解決這個問題,可以實現一個 構造器 - 原型 模式的組合。可以使用 構造函數 讓屬於該對象的數據值保持私有和唯一。那些可在所有對象之間共享的操作數據通用方法,可以添加到 原型對象 上。
我希望這篇文章能夠幫助讀者詳細了解原型屬性及其用法。如果你對本文中描述的概念有任何疑問,請隨時與我聯繫:
https://twitter.com/aparna_joshi_
https://aparnajoshi.netlify.app/javascript-prototype-inner-workings-of-objects