2024 年 1 月 24 日

原型方法,沒有 __proto__ 的物件

在本節的第一章中,我們提到有設定原型的現代方法。

使用 obj.__proto__ 設定或讀取原型被視為過時且不建議使用(已移至 JavaScript 標準的「附錄 B」,僅供瀏覽器使用)。

取得/設定原型的現代方法如下:

唯一不會被批評的 __proto__ 用法,是在建立新物件時作為屬性:{ __proto__: ... }

不過,這也有專門的方法:

例如:

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.setPrototypeOfObject.getPrototypeOf 被新增到標準中,以執行與 __proto__ 相同的功能。由於 __proto__ 已在各處實際實作,因此它已被棄用,並進入標準的附錄 B,也就是:對於非瀏覽器環境而言是可選的。
  • 後來,在 2022 年,官方允許在物件文字 {...} 中使用 __proto__(從附錄 B 中移除),但不能作為 getter/setter obj.__proto__(仍保留在附錄 B 中)。

為什麼 __proto__getPrototypeOf/setPrototypeOf 函式取代?

為什麼 __proto__ 被部分恢復,並允許在 {...} 中使用,但不能作為 getter/setter?

這是一個有趣的問題,需要我們了解為什麼 __proto__ 不好。

我們很快就會得到答案。

如果速度很重要,請勿變更現有物件的 [[Prototype]]

技術上,我們可以隨時取得/設定 [[Prototype]]。但通常我們只在物件建立時設定一次,不再修改它:rabbit 繼承自 animal,而且不會改變。

JavaScript 引擎針對此功能進行了高度最佳化。使用 Object.setPrototypeOfobj.__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

摘要

  • 若要使用指定的原型建立物件,請使用

    Object.create 提供了一個簡單的方法來淺層複製具有所有描述符的物件

    let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
  • 取得/設定原型的現代方法為

  • 不建議使用內建 __proto__ getter/setter 來取得/設定原型,它現在位於規範的附錄 B 中。

  • 我們還涵蓋了使用 Object.create(null){__proto__: null} 建立的無原型物件。

    這些物件用作字典,用於儲存任何(可能是使用者產生的)金鑰。

    通常,物件會從 Object.prototype 繼承內建方法和 __proto__ getter/setter,使對應的金鑰「被佔用」,並可能造成副作用。使用 null 原型,物件會真正地為空。

任務

重要性:5

有一個物件 dictionary,建立為 Object.create(null),用於儲存任何 key/value 成對資料。

加入方法 dictionary.toString(),它應傳回金鑰的逗號分隔清單。您的 toString 不應出現在物件上的 for..in 中。

以下是它的運作方式

let dictionary = Object.create(null);

// your code to add dictionary.toString method

// add some data
dictionary.apple = "Apple";
dictionary.__proto__ = "test"; // __proto__ is a regular property key here

// only apple and __proto__ are in the loop
for(let key in dictionary) {
  alert(key); // "apple", then "__proto__"
}

// your toString in action
alert(dictionary); // "apple,__proto__"

該方法可以使用 Object.keys 取得所有可列舉金鑰,並輸出其清單。

若要使 toString 不可列舉,我們使用屬性描述符來定義它。Object.create 的語法允許我們提供一個物件,其中包含屬性描述符作為第二個引數。

let dictionary = Object.create(null, {
  toString: { // define toString property
    value() { // the value is a function
      return Object.keys(this).join();
    }
  }
});

dictionary.apple = "Apple";
dictionary.__proto__ = "test";

// apple and __proto__ is in the loop
for(let key in dictionary) {
  alert(key); // "apple", then "__proto__"
}

// comma-separated list of properties by toString
alert(dictionary); // "apple,__proto__"

當我們使用描述符建立屬性時,其旗標預設為 false。因此,在上述程式碼中,dictionary.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,因為它實際上是點之前的物件。

所以只有第一個呼叫顯示 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
教學課程地圖

留言

留言前請先閱讀…
  • 如果您有改進建議 - 請 提交 GitHub 問題 或提出拉取請求,而不是留言。
  • 如果您無法理解文章中的某些內容 - 請詳細說明。
  • 要插入一些程式碼,請使用 <code> 標籤,對於多行 - 將它們包裝在 <pre> 標籤中,對於超過 10 行 - 使用沙箱 (plnkrjsbincodepen…)