在本節的第一章中,我們提到有設定原型的現代方法。
使用 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…)