在本節的第一章中,我們提到有設定原型的現代方法。
使用 obj.__proto__ 設定或讀取原型被視為過時且不建議使用(已移至 JavaScript 標準的「附錄 B」,僅供瀏覽器使用)。
取得/設定原型的現代方法如下:
- Object.getPrototypeOf(obj) – 傳回
obj的[[Prototype]]。 - Object.setPrototypeOf(obj, proto) – 將
obj的[[Prototype]]設定為proto。
唯一不會被批評的 __proto__ 用法,是在建立新物件時作為屬性:{ __proto__: ... }。
不過,這也有專門的方法:
- Object.create(proto[, descriptors]) – 建立一個空的物件,其
proto為[[Prototype]],並具有選用的屬性描述符。
例如:
let animal = {
eats: true
};
// create a new object with animal as a prototype
let rabbit = Object.create(animal); // same as {__proto__: animal}
alert(rabbit.eats); // true
alert(Object.getPrototypeOf(rabbit) === animal); // true
Object.setPrototypeOf(rabbit, {}); // change the prototype of rabbit to {}
Object.create 方法功能更強大,因為它有一個可選的第二個參數:屬性描述符。
我們可以在此為新物件提供其他屬性,如下所示
let animal = {
eats: true
};
let rabbit = Object.create(animal, {
jumps: {
value: true
}
});
alert(rabbit.jumps); // true
描述符的格式與章節 屬性標誌和描述符 中描述的格式相同。
我們可以使用 Object.create 來執行比在 for..in 中複製屬性更強大的物件複製。
let clone = Object.create(
Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj)
);
此呼叫會建立 obj 的完全精確副本,包括所有屬性:可列舉和不可列舉、資料屬性和 setter/getter,以及正確的 [[Prototype]]。
簡史
管理 [[Prototype]] 的方法有很多。這是怎麼發生的?為什麼?
這是出於歷史原因。
原型繼承自語言誕生以來就存在,但管理它的方法隨著時間而演變。
- 建構函式的
prototype屬性從很早以前就開始運作。這是建立具有給定原型的物件最古老的方法。 - 後來,在 2012 年,
Object.create出現在標準中。它提供了建立具有給定原型的物件的能力,但沒有提供取得/設定它的能力。一些瀏覽器實作了非標準的__proto__存取器,允許使用者隨時取得/設定原型,以賦予開發人員更大的彈性。 - 後來,在 2015 年,
Object.setPrototypeOf和Object.getPrototypeOf被新增到標準中,以執行與__proto__相同的功能。由於__proto__已在各處實際實作,因此它已被棄用,並進入標準的附錄 B,也就是:對於非瀏覽器環境而言是可選的。 - 後來,在 2022 年,官方允許在物件文字
{...}中使用__proto__(從附錄 B 中移除),但不能作為 getter/setterobj.__proto__(仍保留在附錄 B 中)。
為什麼 __proto__ 被 getPrototypeOf/setPrototypeOf 函式取代?
為什麼 __proto__ 被部分恢復,並允許在 {...} 中使用,但不能作為 getter/setter?
這是一個有趣的問題,需要我們了解為什麼 __proto__ 不好。
我們很快就會得到答案。
[[Prototype]]技術上,我們可以隨時取得/設定 [[Prototype]]。但通常我們只在物件建立時設定一次,不再修改它:rabbit 繼承自 animal,而且不會改變。
JavaScript 引擎針對此功能進行了高度最佳化。使用 Object.setPrototypeOf 或 obj.__proto__=「動態」變更原型是一個非常慢的運算,因為它會破壞物件屬性存取運算的內部最佳化。因此,除非您知道自己在做什麼,或 JavaScript 速度對您來說完全不重要,否則請避免這麼做。
「非常單純」的物件
我們知道,物件可以用作關聯陣列,來儲存鍵值對。
…但如果我們嘗試在其中儲存使用者提供的鍵(例如使用者輸入的字典),我們會看到一個有趣的錯誤:所有鍵都運作良好,除了 "__proto__"。
查看範例
let obj = {};
let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";
alert(obj[key]); // [object Object], not "some value"!
在此,如果使用者輸入 __proto__,第 4 行的指派會被忽略!
對於非開發人員來說,這肯定會令人驚訝,但對我們來說很容易理解。__proto__ 屬性很特別:它必須是物件或 null。字串不能成為原型。這就是為什麼將字串指派給 __proto__ 會被忽略的原因。
但我們並非有意實作這種行為,對吧?我們想要儲存鍵值對,而名為 "__proto__" 的鍵沒有正確儲存。所以這是一個錯誤!
這裡的後果並不可怕。但在其他情況下,我們可能會在 obj 中儲存物件而非字串,然後原型確實會被變更。結果,執行會以完全出乎意料的方式出錯。
更糟的是,開發人員通常根本不會想到這種可能性。這使得此類錯誤難以察覺,甚至會將它們變成漏洞,特別是在伺服器端使用 JavaScript 時。
在指派給 obj.toString 時也可能發生意外情況,因為它是一個內建物件方法。
我們如何避免這個問題?
首先,我們可以改用 Map 來儲存,而不是純粹的物件,這樣一切都很好
let map = new Map();
let key = prompt("What's the key?", "__proto__");
map.set(key, "some value");
alert(map.get(key)); // "some value" (as intended)
…但 Object 語法通常更具吸引力,因為它更簡潔。
幸運的是,我們可以使用物件,因為語言建立者早已考慮過這個問題。
我們知道,__proto__ 不是物件的屬性,而是 Object.prototype 的存取器屬性
因此,如果讀取或設定 obj.__proto__,就會從其原型呼叫對應的 getter/setter,並取得/設定 [[Prototype]]。
正如本教學節的開頭所述:__proto__ 是存取 [[Prototype]] 的一種方式,它本身不是 [[Prototype]]。
現在,如果我們打算將物件用作關聯陣列,並擺脫此類問題,我們可以使用一個小技巧來做到這一點
let obj = Object.create(null);
// or: obj = { __proto__: null }
let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";
alert(obj[key]); // "some value"
Object.create(null) 會建立一個沒有原型的空物件([[Prototype]] 為 null)
因此,沒有繼承的 __proto__ getter/setter。現在它被視為常規資料屬性處理,因此上面的範例運作正常。
我們可以將此類物件稱為「非常純粹」或「純字典」物件,因為它們甚至比常規純粹物件 {...} 更簡單。
缺點是此類物件缺乏任何內建物件方法,例如 toString
let obj = Object.create(null);
alert(obj); // Error (no toString)
…但這通常對關聯陣列來說沒問題。
請注意,大多數與物件相關的方法都是 Object.something(...),例如 Object.keys(obj) – 它們不在原型中,因此它們將持續在這些物件上運作
let chineseDictionary = Object.create(null);
chineseDictionary.hello = "你好";
chineseDictionary.bye = "再见";
alert(Object.keys(chineseDictionary)); // hello,bye
摘要
-
若要使用指定的原型建立物件,請使用
- 文字語法:
{ __proto__: ... },允許指定多個屬性 - 或 Object.create(proto[, descriptors]),允許指定屬性描述符。
Object.create提供了一個簡單的方法來淺層複製具有所有描述符的物件let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj)); - 文字語法:
-
取得/設定原型的現代方法為
- Object.getPrototypeOf(obj) – 傳回
obj的[[Prototype]](與__proto__getter 相同)。 - Object.setPrototypeOf(obj, proto) – 將
obj的[[Prototype]]設定為proto(與__proto__setter 相同)。
- Object.getPrototypeOf(obj) – 傳回
-
不建議使用內建
__proto__getter/setter 來取得/設定原型,它現在位於規範的附錄 B 中。 -
我們還涵蓋了使用
Object.create(null)或{__proto__: null}建立的無原型物件。這些物件用作字典,用於儲存任何(可能是使用者產生的)金鑰。
通常,物件會從
Object.prototype繼承內建方法和__proto__getter/setter,使對應的金鑰「被佔用」,並可能造成副作用。使用null原型,物件會真正地為空。
留言
<code>標籤,對於多行 - 將它們包裝在<pre>標籤中,對於超過 10 行 - 使用沙箱 (plnkr,jsbin,codepen…)