Javascript 原型方法,沒有 __proto__ 的對象

2023-02-17 10:52 更新

在這部分內(nèi)容的第一章中,我們提到了設(shè)置原型的現(xiàn)代方法。

使用 obj.__proto__ 設(shè)置或讀取原型被認(rèn)為已經(jīng)過時且不推薦使用(deprecated)了(已經(jīng)被移至 JavaScript 規(guī)范的附錄 B,意味著僅適用于瀏覽器)。

現(xiàn)代的獲取/設(shè)置原型的方法有:

__proto__ 不被反對的唯一的用法是在創(chuàng)建新對象時,將其用作屬性:{ __proto__: ... }。

雖然,也有一種特殊的方法:

例如:

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ā)生了什么?為什么會這樣?

這是歷史原因。

原型繼承從一開始就存在于語言中,但管理它的方式隨著時間的推移而演變。

  • 構(gòu)造函數(shù)的 ?"prototype"? 屬性自古以來就起作用。這是使用給定原型創(chuàng)建對象的最古老的方式。
  • 之后,在 2012 年,?Object.create? 出現(xiàn)在標(biāo)準(zhǔn)中。它提供了使用給定原型創(chuàng)建對象的能力。但沒有提供 get/set 它的能力。一些瀏覽器實現(xiàn)了非標(biāo)準(zhǔn)的 ?__proto__? 訪問器,以為開發(fā)者提供更多的靈活性。
  • 之后,在 2015 年,?Object.setPrototypeOf? 和 ?Object.getPrototypeOf? 被加入到標(biāo)準(zhǔn)中,執(zhí)行與 ?__proto__? 相同的功能。由于 ?__proto__? 實際上已經(jīng)在所有地方都得到了實現(xiàn),但它已過時,所以被加入到該標(biāo)準(zhǔn)的附件 B 中,即:在非瀏覽器環(huán)境下,它的支持是可選的。
  • 之后,在 2022 年,官方允許在對象字面量 ?{...}? 中使用 ?__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í)行速度對你來說完全不重要,否則請避免使用它。

"Very plain" objects

我們知道,對象可以用作關(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

總結(jié)

  • 要使用給定的原型創(chuàng)建對象,使用:
  • Object.create 提供了一種簡單的方式來淺拷貝對象及其所有屬性描述符(descriptors)。

    let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
    
  • 設(shè)置和訪問原型的現(xiàn)代方法有:
  • 不推薦使用內(nèi)建的的 ?__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 時,對象才真正是空的。

任務(wù)


為 dictionary 添加 toString 方法

重要程度: 5

這兒有一個通過 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)行回顧。


調(diào)用方式的差異

重要程度: 5

讓我們創(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


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號