W3Cschool
恭喜您成為首批注冊用戶
獲得88經(jīng)驗值獎勵
在這部分內(nèi)容的第一章中,我們提到了設(shè)置原型的現(xiàn)代方法。
使用 obj.__proto__
設(shè)置或讀取原型被認(rèn)為已經(jīng)過時且不推薦使用(deprecated)了(已經(jīng)被移至 JavaScript 規(guī)范的附錄 B,意味著僅適用于瀏覽器)。
現(xiàn)代的獲取/設(shè)置原型的方法有:
obj
? 的 ?[[Prototype]]
?。obj
? 的 ?[[Prototype]]
? 設(shè)置為 ?proto
?。__proto__
不被反對的唯一的用法是在創(chuàng)建新對象時,將其用作屬性:{ __proto__: ... }
。
雖然,也有一種特殊的方法:
proto
? 作為 ?[[Prototype]]
? 和可選的屬性描述來創(chuàng)建一個空對象。例如:
let animal = {
eats: true
};
// 創(chuàng)建一個以 animal 為原型的新對象
let rabbit = Object.create(animal); // 與 {__proto__: animal} 相同
alert(rabbit.eats); // true
alert(Object.getPrototypeOf(rabbit) === animal); // true
Object.setPrototypeOf(rabbit, {}); // 將 rabbit 的原型修改為 {}
Object.create
方法更強大,因為它有一個可選的第二參數(shù):屬性描述器。
我們可以在此處為新對象提供額外的屬性,就像這樣:
let animal = {
eats: true
};
let rabbit = Object.create(animal, {
jumps: {
value: true
}
});
alert(rabbit.jumps); // true
描述器的格式與 屬性標(biāo)志和屬性描述符 一章中所講的一樣。
我們可以使用 Object.create
來實現(xiàn)比復(fù)制 for..in
循環(huán)中的屬性更強大的對象克隆方式:
let clone = Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
);
此調(diào)用可以對 obj
進(jìn)行真正準(zhǔn)確地拷貝,包括所有的屬性:可枚舉和不可枚舉的,數(shù)據(jù)屬性和 setters/getters —— 包括所有內(nèi)容,并帶有正確的 [[Prototype]]
。
有這么多可以處理 ?[[Prototype]]
? 的方式。發(fā)生了什么?為什么會這樣?
這是歷史原因。
原型繼承從一開始就存在于語言中,但管理它的方式隨著時間的推移而演變。
"prototype"
? 屬性自古以來就起作用。這是使用給定原型創(chuàng)建對象的最古老的方式。Object.create
? 出現(xiàn)在標(biāo)準(zhǔn)中。它提供了使用給定原型創(chuàng)建對象的能力。但沒有提供 get/set 它的能力。一些瀏覽器實現(xiàn)了非標(biāo)準(zhǔn)的 ?__proto__
? 訪問器,以為開發(fā)者提供更多的靈活性。Object.setPrototypeOf
? 和 ?Object.getPrototypeOf
? 被加入到標(biāo)準(zhǔn)中,執(zhí)行與 ?__proto__
? 相同的功能。由于 ?__proto__
? 實際上已經(jīng)在所有地方都得到了實現(xiàn),但它已過時,所以被加入到該標(biāo)準(zhǔn)的附件 B 中,即:在非瀏覽器環(huán)境下,它的支持是可選的。{...}
? 中使用 ?__proto__
?(從附錄 B 中移出來了),但不能用作 getter/setter ?obj.__proto__
?(仍在附錄 B 中)。為什么要用函數(shù) getPrototypeOf/setPrototypeOf
取代 __proto__
?
為什么 __proto__
被部分認(rèn)可并允許在 {...}
中使用,但仍不能用作 getter/setter?
這是一個有趣的問題,需要我們理解為什么 __proto__
不好。
很快我們就會看到答案。
如果速度很重要,就請不要修改已存在的對象的 ?
[[Prototype]]
?從技術(shù)上來講,我們可以在任何時候 get/set
[[Prototype]]
。但是通常我們只在創(chuàng)建對象的時候設(shè)置它一次,自那之后不再修改:rabbit
繼承自animal
,之后不再更改。
并且,JavaScript 引擎對此進(jìn)行了高度優(yōu)化。用
Object.setPrototypeOf
或obj.__proto__=
“即時”更改原型是一個非常緩慢的操作,因為它破壞了對象屬性訪問操作的內(nèi)部優(yōu)化。因此,除非你知道自己在做什么,或者 JavaScript 的執(zhí)行速度對你來說完全不重要,否則請避免使用它。
我們知道,對象可以用作關(guān)聯(lián)數(shù)組(associative arrays)來存儲鍵/值對。
……但是如果我們嘗試在其中存儲 用戶提供的 鍵(例如:一個用戶輸入的字典),我們可以發(fā)現(xiàn)一個有趣的小故障:所有的鍵都正常工作,除了 "__proto__"
。
看一下這個例子:
let obj = {};
let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";
alert(obj[key]); // [object Object],并不是 "some value"!
這里如果用戶輸入 __proto__
,那么在第四行的賦值會被忽略!
對于非開發(fā)者來說,這肯定很令人驚訝,但對我們來說卻是可以理解的。__proto__
屬性很特殊:它必須是一個對象或者 null
。字符串不能成為原型。這就是為什么將字符串賦值給 __proto__
會被忽略。
但我們不是 打算 實現(xiàn)這種行為,對吧?我們想要存儲鍵值對,然而鍵名為 "__proto__"
的鍵值對沒有被正確存儲。所以這是一個 bug。
這里的后果并沒有很嚴(yán)重。但在其他情況下,我們可能會在 obj
中存儲對象而不是字符串,則原型確實會被改變。結(jié)果,執(zhí)行將以完全意想不到的方式出錯。
最可怕的是 —— 通常開發(fā)者完全不會考慮到這一點。這讓此類 bug 很難被發(fā)現(xiàn),甚至變成漏洞,尤其是在 JavaScript 被用在服務(wù)端的時候。
對 obj.toString
進(jìn)行賦值時也可能發(fā)生意想不到的事情,因為它是一個內(nèi)建的對象方法。
我們怎么避免這樣的問題呢?
首先,我們可以改用 Map
來代替普通對象進(jìn)行存儲,這樣一切都迎刃而解:
let map = new Map();
let key = prompt("What's the key?", "__proto__");
map.set(key, "some value");
alert(map.get(key)); // "some value"(符合預(yù)期)
……但 Object
語法通常更吸引人,因為它更簡潔。
幸運的是,我們 可以 使用對象,因為 JavaScript 語言的制造者很久以前就考慮過這個問題。
正如我們所知道的,__proto__
不是對象的屬性,而是 Object.prototype
的訪問器屬性:
因此,如果 obj.__proto__
被讀取或者賦值,那么對應(yīng)的 getter/setter 會被從它的原型中調(diào)用,它會 set/get [[Prototype]]
。
就像在本部分教程的開頭所說的那樣:__proto__
是一種訪問 [[Prototype]]
的方式,而不是 [[prototype]]
本身。
現(xiàn)在,我們想要將一個對象用作關(guān)聯(lián)數(shù)組,并且擺脫此類問題,我們可以使用一些小技巧:
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)
創(chuàng)建了一個空對象,這個對象沒有原型([[Prototype]]
是 null
):
因此,它沒有繼承 __proto__
的 getter/setter 方法。現(xiàn)在,它被作為正常的數(shù)據(jù)屬性進(jìn)行處理,因此上面的這個示例能夠正常工作。
我們可以把這樣的對象稱為 “very plain” 或 “pure dictionary” 對象,因為它們甚至比通常的普通對象(plain object){...}
還要簡單。
缺點是這樣的對象沒有任何內(nèi)建的對象的方法,例如 toString
:
let obj = Object.create(null);
alert(obj); // Error (no toString)
……但是它們通常對關(guān)聯(lián)數(shù)組而言還是很友好。
請注意,大多數(shù)與對象相關(guān)的方法都是 Object.something(...)
,例如 Object.keys(obj)
—— 它們不在 prototype 中,因此在 “very plain” 對象中它們還是可以繼續(xù)使用:
let chineseDictionary = Object.create(null);
chineseDictionary.hello = "你好";
chineseDictionary.bye = "再見";
alert(Object.keys(chineseDictionary)); // hello,bye
{ __proto__: ... }
?,允許指定多個屬性Object.create
提供了一種簡單的方式來淺拷貝對象及其所有屬性描述符(descriptors)。
let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
obj
? 的 ?[[Prototype]]
?(與 ?__proto__
? 的 getter 相同)。obj
? 的 ?[[Prototype]]
? 設(shè)置為 ?proto
?(與 ?__proto__
? 的 setter 相同)。__proto__
? getter/setter 獲取/設(shè)置原型,它現(xiàn)在在 ECMA 規(guī)范的附錄 B 中。Object.create(null)
? 或 ?{__proto__: null}
? 創(chuàng)建的無原型的對象。這些對象被用作字典,以存儲任意(可能是用戶生成的)鍵。
通常,對象會從 Object.prototype
繼承內(nèi)建的方法和 __proto__
getter/setter,會占用相應(yīng)的鍵,且可能會導(dǎo)致副作用。原型為 null
時,對象才真正是空的。
這兒有一個通過 Object.create(null)
創(chuàng)建的,用來存儲任意 key/value
對的對象 dictionary
。
為該對象添加 dictionary.toString()
方法,該方法應(yīng)該返回以逗號分隔的所有鍵的列表。你的 toString
方法不應(yīng)該在使用 for...in
循環(huán)遍歷數(shù)組的時候顯現(xiàn)出來。
它的工作方式如下:
let dictionary = Object.create(null);
// 你的添加 dictionary.toString 方法的代碼
// 添加一些數(shù)據(jù)
dictionary.apple = "Apple";
dictionary.__proto__ = "test"; // 這里 __proto__ 是一個常規(guī)的屬性鍵
// 在循環(huán)中只有 apple 和 __proto__
for(let key in dictionary) {
alert(key); // "apple", then "__proto__"
}
// 你的 toString 方法在發(fā)揮作用
alert(dictionary); // "apple,__proto__"
可以使用 Object.keys
獲取所有可枚舉的鍵,并輸出其列表。
為了使 toString
不可枚舉,我們使用一個屬性描述器來定義它。Object.create
語法允許我們?yōu)橐粋€對象提供屬性描述器作為第二參數(shù)。
let dictionary = Object.create(null, {
toString: { // 定義 toString 屬性
value() { // value 是一個 function
return Object.keys(this).join();
}
}
});
dictionary.apple = "Apple";
dictionary.__proto__ = "test";
// apple 和 __proto__ 在循環(huán)中
for(let key in dictionary) {
alert(key); // "apple",然后是 "__proto__"
}
// 通過 toString 處理獲得的以逗號分隔的屬性列表
alert(dictionary); // "apple,__proto__"
當(dāng)我們使用描述器創(chuàng)建一個屬性,它的標(biāo)識默認(rèn)是 false
。因此在上面這段代碼中,dictonary.toString
是不可枚舉的。
請閱讀 屬性標(biāo)志和屬性描述符 一章進(jìn)行回顧。
讓我們創(chuàng)建一個新的 ?rabbit
? 對象:
function Rabbit(name) {
this.name = name;
}
Rabbit.prototype.sayHi = function() {
alert(this.name);
};
let rabbit = new Rabbit("Rabbit");
以下調(diào)用做的是相同的事兒還是不同的?
rabbit.sayHi();
Rabbit.prototype.sayHi();
Object.getPrototypeOf(rabbit).sayHi();
rabbit.__proto__.sayHi();
第一個調(diào)用中 this == rabbit
,其他的 this
等同于 Rabbit.prototype
,因為 this
就是點符號前面的對象。
所以,只有第一個調(diào)用顯示 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
Copyright©2021 w3cschool編程獅|閩ICP備15016281號-3|閩公網(wǎng)安備35020302033924號
違法和不良信息舉報電話:173-0602-2364|舉報郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號
聯(lián)系方式:
更多建議: