在這部分內容的第壹章中,我們提到了設置原型的現代方法。
使用 obj.__proto__
設置或讀取原型被認爲已經過時且不推薦使用(deprecated)了(已經被移至 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 }; // 創建壹個以 animal 爲原型的新對象 let rabbit = Object.create(animal); // 與 {__proto__: animal} 相同 alert(rabbit.eats); // true alert(Object.getPrototypeOf(rabbit) === animal); // true Object.setPrototypeOf(rabbit, {}); // 將 rabbit 的原型修改爲 {}
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
進行真正准確地拷貝,包括所有的屬性:可枚舉和不可枚舉的,數據屬性和 setters/getters —— 包括所有內容,並帶有正確的 [[Prototype]]
。
有這麽多可以處理 [[Prototype]]
的方式。發生了什麽?爲什麽會這樣?
這是曆史原因。
原型繼承從壹開始就存在于語言中,但管理它的方式隨著時間的推移而演變。
構造函數的 "prototype"
屬性自古以來就起作用。這是使用給定原型創建對象的最古老的方式。
之後,在 2012 年,Object.create
出現在標准中。它提供了使用給定原型創建對象的能力。但沒有提供 get/set 它的能力。壹些浏覽器實現了非標准的 __proto__
訪問器,以爲開發者提供更多的靈活性。
之後,在 2015 年,Object.setPrototypeOf
和 Object.getPrototypeOf
被加入到標准中,執行與 __proto__
相同的功能。由于 __proto__
實際上已經在所有地方都得到了實現,但它已過時,所以被加入到該標准的附件 B 中,即:在非浏覽器環境下,它的支持是可選的。
之後,在 2022 年,官方允許在對象字面量 {...}
中使用 __proto__
(從附錄 B 中移出來了),但不能用作 getter/setter obj.__proto__
(仍在附錄 B 中)。
爲什麽要用函數 getPrototypeOf/setPrototypeOf
取代 __proto__
?
爲什麽 __proto__
被部分認可並允許在 {...}
中使用,但仍不能用作 getter/setter?
這是壹個有趣的問題,需要我們理解爲什麽 __proto__
不好。
很快我們就會看到答案。
如果速度很重要,就請不要修改已存在的對象的 [[Prototype]]
從技術上來講,我們可以在任何時候 get/set [[Prototype]]
。但是通常我們只在創建對象的時候設置它壹次,自那之後不再修改:rabbit
繼承自 animal
,之後不再更改。
並且,JavaScript 引擎對此進行了高度優化。用 Object.setPrototypeOf
或 obj.__proto__=
“即時”更改原型是壹個非常緩慢的操作,因爲它破壞了對象屬性訪問操作的內部優化。因此,除非妳知道自己在做什麽,或者 JavaScript 的執行速度對妳來說完全不重要,否則請避免使用它。
我們知道,對象可以用作關聯數組(associative arrays)來存儲鍵/值對。
……但是如果我們嘗試在其中存儲 用戶提供的 鍵(例如:壹個用戶輸入的字典),我們可以發現壹個有趣的小故障:所有的鍵都正常工作,除了 "__proto__"
。
看壹下這個例子:
let obj = {}; let key = prompt("What's the key?", "__proto__"); obj[key] = "some value"; alert(obj[key]); // [object Object],並不是 "some value"!
這裏如果用戶輸入 __proto__
,那麽在第四行的賦值會被忽略!
對于非開發者來說,這肯定很令人驚訝,但對我們來說卻是可以理解的。__proto__
屬性很特殊:它必須是壹個對象或者 null
。字符串不能成爲原型。這就是爲什麽將字符串賦值給 __proto__
會被忽略。
但我們不是 打算 實現這種行爲,對吧?我們想要存儲鍵值對,然而鍵名爲 "__proto__"
的鍵值對沒有被正確存儲。所以這是壹個 bug。
這裏的後果並沒有很嚴重。但在其他情況下,我們可能會在 obj
中存儲對象而不是字符串,則原型確實會被改變。結果,執行將以完全意想不到的方式出錯。
最可怕的是 —— 通常開發者完全不會考慮到這壹點。這讓此類 bug 很難被發現,甚至變成漏洞,尤其是在 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"(符合預期)
……但 Object
語法通常更吸引人,因爲它更簡潔。
幸運的是,我們 可以 使用對象,因爲 JavaScript 語言的制造者很久以前就考慮過這個問題。
正如我們所知道的,__proto__
不是對象的屬性,而是 Object.prototype
的訪問器屬性:
因此,如果 obj.__proto__
被讀取或者賦值,那麽對應的 getter/setter 會被從它的原型中調用,它會 set/get [[Prototype]]
。
就像在本部分教程的開頭所說的那樣:__proto__
是壹種訪問 [[Prototype]]
的方式,而不是 [[prototype]]
本身。
現在,我們想要將壹個對象用作關聯數組,並且擺脫此類問題,我們可以使用壹些小技巧:
let obj = Object.create(null); // 或者: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 方法。現在,它被作爲正常的數據屬性進行處理,因此上面的這個示例能夠正常工作。
我們可以把這樣的對象稱爲 “very plain” 或 “pure dictionary” 對象,因爲它們甚至比通常的普通對象(plain object){...}
還要簡單。
缺點是這樣的對象沒有任何內建的對象的方法,例如 toString
:
let obj = Object.create(null); alert(obj); // Error (no toString)
……但是它們通常對關聯數組而言還是很友好。
請注意,大多數與對象相關的方法都是 Object.something(...)
,例如 Object.keys(obj)
—— 它們不在 prototype 中,因此在 “very plain” 對象中它們還是可以繼續使用:
let chineseDictionary = Object.create(null); chineseDictionary.hello = "妳好"; chineseDictionary.bye = "再見"; alert(Object.keys(chineseDictionary)); // hello,bye
要使用給定的原型創建對象,使用:
Object.create
提供了壹種簡單的方式來淺拷貝對象及其所有屬性描述符(descriptors)。
let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
字面量語法:{ __proto__: ... }
,允許指定多個屬性
或 Object.create(proto, [descriptors]),允許指定屬性描述符。
設置和訪問原型的現代方法有:
Object.getPrototypeOf(obj) —— 返回對象 obj
的 [[Prototype]]
(與 __proto__
的 getter 相同)。
Object.setPrototypeOf(obj, proto) —— 將對象 obj
的 [[Prototype]]
設置爲 proto
(與 __proto__
的 setter 相同)。
不推薦使用內建的 __proto__
getter/setter 獲取/設置原型,它現在在 ECMA 規範的附錄 B 中。
我們還介紹了使用 Object.create(null)
或 {__proto__: null}
創建的無原型的對象。
這些對象被用作字典,以存儲任意(可能是用戶生成的)鍵。
通常,對象會從 Object.prototype
繼承內建的方法和 __proto__
getter/setter,會占用相應的鍵,且可能會導致副作用。原型爲 null
時,對象才真正是空的。
重要程度: 5
這兒有壹個通過 Object.create(null)
創建的,用來存儲任意 key/value
對的對象 dictionary
。
爲該對象添加 dictionary.toString()
方法,該方法應該返回以逗號分隔的所有鍵的列表。妳的 toString
方法不應該在使用 for...in
循環遍曆數組的時候顯現出來。
它的工作方式如下:
let dictionary = Object.create(null); // 妳的添加 dictionary.toString 方法的代碼 // 添加壹些數據 dictionary.apple = "Apple"; dictionary.__proto__ = "test"; // 這裏 __proto__ 是壹個常規的屬性鍵 // 在循環中只有 apple 和 __proto__ for(let key in dictionary) { alert(key); // "apple", then "__proto__" } // 妳的 toString 方法在發揮作用 alert(dictionary); // "apple,__proto__"
可以使用 Object.keys
獲取所有可枚舉的鍵,並輸出其列表。
爲了使 toString
不可枚舉,我們使用壹個屬性描述器來定義它。Object.create
語法允許我們爲壹個對象提供屬性描述器作爲第二參數。
let dictionary = Object.create(null, { toString: { // 定義 toString 屬性 value() { // value 是壹個 function return Object.keys(this).join(); } } }); dictionary.apple = "Apple"; dictionary.__proto__ = "test"; // apple 和 __proto__ 在循環中 for(let key in dictionary) { alert(key); // "apple",然後是 "__proto__" } // 通過 toString 處理獲得的以逗號分隔的屬性列表 alert(dictionary); // "apple,__proto__"
當我們使用描述器創建壹個屬性,它的標識默認是 false
。因此在上面這段代碼中,dictonary.toString
是不可枚舉的。
請閱讀 屬性標志和屬性描述符 壹章進行回顧。
重要程度: 5
讓我們創建壹個新的 rabbit
對象:
function Rabbit(name) { this.name = name; } Rabbit.prototype.sayHi = function() { alert(this.name); }; let rabbit = new Rabbit("Rabbit");
以下調用做的是相同的事兒還是不同的?
rabbit.sayHi(); Rabbit.prototype.sayHi(); Object.getPrototypeOf(rabbit).sayHi(); rabbit.__proto__.sayHi();
第壹個調用中 this == rabbit
,其他的 this
等同于 Rabbit.prototype
,因爲 this
就是點符號前面的對象。
所以,只有第壹個調用顯示 Rabbit
,其他的都顯示的是 undefined
:
function Rabbit(name) { this.name = name; } Rabbit.prototype.sayHi = function() { alert( this.name ); } let rabbit = new Rabbit("Rabbit"); rabbit.sayHi(); // Rabbit Rabbit.prototype.sayHi(); // undefined Object.getPrototypeOf(rabbit).sayHi(); // undefined rabbit.__proto__.sayHi(); // undefined