for-in
迴圈中隱藏屬性JSON.stringify()
中隱藏自訂屬性可列舉性是物件屬性的屬性。在本章中,我們將深入探討如何使用它,以及它如何影響 Object.keys()
和 Object.assign()
等操作。
必備知識:屬性屬性
在本章中,您應熟悉屬性屬性。如果您不熟悉,請查看 §9「屬性屬性:簡介」。
為了展示各種操作如何受到可列舉性的影響,我們使用以下原型為 proto
的物件 obj
。
const protoEnumSymbolKey = Symbol('protoEnumSymbolKey');
const protoNonEnumSymbolKey = Symbol('protoNonEnumSymbolKey');
const proto = Object.defineProperties({}, {
protoEnumStringKey: {
value: 'protoEnumStringKeyValue',
enumerable: true,
},
[protoEnumSymbolKey]: {
value: 'protoEnumSymbolKeyValue',
enumerable: true,
},
protoNonEnumStringKey: {
value: 'protoNonEnumStringKeyValue',
enumerable: false,
},
[protoNonEnumSymbolKey]: {
value: 'protoNonEnumSymbolKeyValue',
enumerable: false,
},
});
const objEnumSymbolKey = Symbol('objEnumSymbolKey');
const objNonEnumSymbolKey = Symbol('objNonEnumSymbolKey');
const obj = Object.create(proto, {
objEnumStringKey: {
value: 'objEnumStringKeyValue',
enumerable: true,
},
[objEnumSymbolKey]: {
value: 'objEnumSymbolKeyValue',
enumerable: true,
},
objNonEnumStringKey: {
value: 'objNonEnumStringKeyValue',
enumerable: false,
},
[objNonEnumSymbolKey]: {
value: 'objNonEnumSymbolKeyValue',
enumerable: false,
},
});
操作 | 字串鍵 | 符號鍵 | 繼承 | |
---|---|---|---|---|
Object.keys() |
ES5 | ✔ |
✘ |
✘ |
Object.values() |
ES2017 | ✔ |
✘ |
✘ |
Object.entries() |
ES2017 | ✔ |
✘ |
✘ |
展開 {...x} |
ES2018 | ✔ |
✔ |
✘ |
Object.assign() |
ES6 | ✔ |
✔ |
✘ |
JSON.stringify() |
ES5 | ✔ |
✘ |
✘ |
for-in |
ES1 | ✔ |
✘ |
✔ |
以下操作(總結於表 2)僅考慮可列舉的屬性
Object.keys()
[ES5] 傳回可列舉的自身字串鍵值屬性的鍵值。
Object.values()
[ES2017] 傳回可列舉的自身字串鍵值屬性的值。
Object.entries()
[ES2017] 傳回可列舉的自身字串鍵值屬性的鍵值對。(注意 Object.fromEntries()
接受符號做為鍵值,但只會建立可列舉的屬性。)
散佈到物件文字中 [ES2018] 僅考慮自身可列舉的屬性(字串鍵值或符號鍵值)。
Object.assign()
[ES6] 僅複製可列舉的自身屬性(字串鍵值或符號鍵值)。
JSON.stringify()
[ES5] 僅將可列舉的自身字串鍵值屬性字串化。
for-in
迴圈 [ES1] 遍歷自身和繼承的可列舉字串鍵值屬性的鍵值。
for-in
是唯一內建操作,其中繼承屬性的可列舉性很重要。所有其他操作僅適用於自身屬性。
操作 | 字串鍵值 | 符號鍵值 | 繼承 | |
---|---|---|---|---|
Object.getOwnPropertyNames() |
ES5 | ✔ |
✘ |
✘ |
Object.getOwnPropertySymbols() |
ES6 | ✘ |
✔ |
✘ |
Reflect.ownKeys() |
ES6 | ✔ |
✔ |
✘ |
Object.getOwnPropertyDescriptors() |
ES2017 | ✔ |
✔ |
✘ |
以下操作(總結於表 3)同時考慮可列舉和不可列舉的屬性
Object.getOwnPropertyNames()
[ES5] 列出所有自身字串鍵值屬性的鍵值。
Object.getOwnPropertySymbols()
[ES6] 列出所有自身符號鍵值屬性的鍵值。
Reflect.ownKeys()
[ES6] 列出所有自身屬性的鍵值。
Object.getOwnPropertyDescriptors()
[ES2017] 列出所有自身屬性的屬性描述符。
> Object.getOwnPropertyDescriptors(obj)
{
objEnumStringKey: {
value: 'objEnumStringKeyValue',
writable: false,
enumerable: true,
configurable: false
},
objNonEnumStringKey: {
value: 'objNonEnumStringKeyValue',
writable: false,
enumerable: false,
configurable: false
},
[objEnumSymbolKey]: {
value: 'objEnumSymbolKeyValue',
writable: false,
enumerable: true,
configurable: false
},
[objNonEnumSymbolKey]: {
value: 'objNonEnumSymbolKeyValue',
writable: false,
enumerable: false,
configurable: false
}
}
內省讓程式可以在執行階段檢查值的結構。它是元程式設計:一般程式設計是撰寫程式;元程式設計是檢查和/或變更程式。
在 JavaScript 中,常見的內省操作有簡短的名稱,而較少使用的操作則有較長的名稱。忽略不可列舉的屬性是常態,這就是為什麼執行此操作的操作有簡短的名稱,而沒有執行此操作的操作有較長的名稱
Object.keys()
忽略不可列舉的屬性。Object.getOwnPropertyNames()
列出所有自有屬性的字串金鑰。然而,Reflect
方法(例如 Reflect.ownKeys()
)偏離此規則,因為 Reflect
提供更多「元」且與代理相關的操作。
此外,做出以下區別(自引入符號的 ES6 以來)
因此,Object.keys()
的更好名稱現在應該是 Object.names()
。
在本節中,我們將簡寫 Object.getOwnPropertyDescriptor()
如下
大多數資料屬性都使用以下屬性建立
其中包括
Object.fromEntries()
最重要的不可列舉屬性為
內建類別的原型屬性
透過使用者定義類別建立的原型屬性
陣列的屬性 .length
字串的屬性 .length
(請注意,原始值的屬性都是唯讀的)
我們將在接下來探討可列舉性的使用案例,這將告訴我們為什麼有些屬性可列舉而有些則不可列舉。
可列舉性是不一致的功能。它確實有使用案例,但總有一些警告。在本節中,我們將探討使用案例和警告。
for-in
迴圈的屬性for-in
迴圈會遍歷物件的所有可列舉字串金鑰屬性,包括自有和繼承的屬性。因此,屬性 enumerable
用於隱藏不應遍歷的屬性。這是 ECMAScript 1 中引入可列舉性的原因。
一般來說,最好避免使用 for-in
。接下來的兩個小節將說明原因。以下函式將幫助我們展示 for-in
的運作方式。
function listPropertiesViaForIn(obj) {
const result = [];
for (const key in obj) {
result.push(key);
}
return result;
}
for-in
處理物件的警告for-in
會反覆處理所有屬性,包括繼承的屬性
const proto = {enumerableProtoProp: 1};
const obj = {
__proto__: proto,
enumerableObjProp: 2,
};
assert.deepEqual(
listPropertiesViaForIn(obj),
['enumerableObjProp', 'enumerableProtoProp']);
對於一般的純粹物件,for-in
看不到繼承的方法,例如 Object.prototype.toString()
,因為它們都是不可列舉的
在使用者定義的類別中,所有繼承的屬性也都是不可列舉的,因此會被忽略
class Person {
constructor(first, last) {
this.first = first;
this.last = last;
}
getName() {
return this.first + ' ' + this.last;
}
}
const jane = new Person('Jane', 'Doe');
assert.deepEqual(
listPropertiesViaForIn(jane),
['first', 'last']);
結論:在物件中,for-in
會考量繼承的屬性,而我們通常希望略過這些屬性。因此,最好將 for-of
迴圈與 Object.keys()
、Object.entries()
等結合使用。
for-in
處理陣列的注意事項陣列和字串中的自有屬性 .length
不可列舉,因此會被 for-in
略過
然而,使用 for-in
迭代陣列索引通常並不安全,因為它會考量繼承的屬性以及不是索引的自有屬性。以下範例說明如果陣列有自有的非索引屬性會發生什麼情況
const arr1 = ['a', 'b'];
assert.deepEqual(
listPropertiesViaForIn(arr1),
['0', '1']);
const arr2 = ['a', 'b'];
arr2.nonIndexProp = 'yes';
assert.deepEqual(
listPropertiesViaForIn(arr2),
['0', '1', 'nonIndexProp']);
結論:for-in
不應使用於迭代陣列索引,因為它會考量索引屬性和非索引屬性
如果您有興趣取得陣列的鍵,請使用陣列方法 .keys()
如果您想要迭代陣列的元素,請使用 for-of
迴圈,它還有額外的優點,也能用於其他可迭代資料結構。
透過讓屬性不可列舉,我們可以將它們隱藏在某些複製作業中。在繼續進行更現代的複製作業之前,讓我們先檢查兩個歷史性的複製作業。
Object.extend()
Prototype 是由 Sam Stephenson 於 2005 年 2 月建立的 JavaScript 框架,作為 Ruby on Rails 中 Ajax 支援的基礎。
Prototype 的 Object.extend(destination, source)
會將 source
中所有可列舉的自有和繼承屬性複製到 destination
的自有屬性中。它的實作方式如下
function extend(destination, source) {
for (var property in source)
destination[property] = source[property];
return destination;
}
如果我們對物件使用 Object.extend()
,我們可以看到它會將繼承的屬性複製到自有屬性中,並略過不可列舉的屬性(它也會略過符號鍵的屬性)。這一切都歸因於 for-in
的運作方式。
const proto = Object.defineProperties({}, {
enumProtoProp: {
value: 1,
enumerable: true,
},
nonEnumProtoProp: {
value: 2,
enumerable: false,
},
});
const obj = Object.create(proto, {
enumObjProp: {
value: 3,
enumerable: true,
},
nonEnumObjProp: {
value: 4,
enumerable: false,
},
});
assert.deepEqual(
extend({}, obj),
{enumObjProp: 3, enumProtoProp: 1});
$.extend()
jQuery 的 $.extend(target, source1, source2, ···)
運作方式類似於 Object.extend()
source1
的所有可列舉的自身和繼承的屬性複製到 target
的自身屬性中。source2
執行相同的動作。以可列舉性為基礎的複製有幾個缺點
雖然可列舉性對於隱藏繼承的屬性很有用,但它主要用於這種方式,因為我們通常只想要將自身屬性複製到自身屬性中。忽略繼承的屬性可以更好地達到相同的目的。
要複製哪些屬性通常取決於手邊的工作;對於所有使用案例來說,單一標誌很少有意義。更好的選擇是提供一個複製操作,其中包含一個謂詞(返回布林值的回呼),告訴它何時忽略屬性。
複製時,可列舉性會方便地隱藏陣列的自身屬性 .length
。但這是一個非常罕見的例外情況:一個既影響兄弟屬性又受兄弟屬性影響的神奇屬性。如果我們自己實作這種神奇屬性,我們將使用(繼承的)取得器和/或設定器,而不是(自身)資料屬性。
Object.assign()
[ES5]在 ES6 中,Object.assign(target, source_1, source_2, ···)
可用於將來源合併到目標中。會考慮來源的所有自身可列舉屬性(使用字串金鑰或符號金鑰)。Object.assign()
使用「取得」操作從來源讀取值,並使用「設定」操作將值寫入目標。
關於可列舉性,Object.assign()
延續了 Object.extend()
和 $.extend()
的傳統。引用 Yehuda Katz
Object.assign
將為所有已經流通的extend()
API 鋪平道路。我們認為,在這些情況下不複製可列舉方法的先例足以讓Object.assign
具有這種行為。
換句話說:Object.assign()
是在考慮從 $.extend()
(和類似函式)升級的途徑下建立的。它的方法比 $.extend
更簡潔,因為它忽略了繼承的屬性。
非可列舉性有幫助的情況很少。一個罕見的範例是 函式庫 fs-extra
最近遇到的問題
內建的 Node.js 模組 fs
有個屬性 .promises
,其中包含一個物件,其為 fs
API 的 Promise 版本。在發生問題時,讀取 .promise
會導致以下警告記錄到主控台
ExperimentalWarning: The fs.promises API is experimental
除了提供自己的功能之外,fs-extra
也重新導出 fs
中的所有內容。對於 CommonJS 模組來說,這表示將 fs
的所有屬性複製到 fs-extra
的 module.exports
(透過 Object.assign()
)。而當 fs-extra
這麼做時,就會觸發警告。這很令人困惑,因為每次載入 fs-extra
時都會發生這種情況。
一個快速解決方法是將屬性 fs.promises
設定為不可列舉。之後,fs-extra
便會忽略它。
如果我們將屬性設定為不可列舉,它將無法再透過 Object.keys()
、for-in
迴圈等方式看到。對於這些機制而言,該屬性是私人的。
然而,這種方法有幾個問題
JSON.stringify()
之外JSON.stringify()
輸出中不包含不可列舉的屬性。因此,我們可以使用可列舉性來決定哪些自己的屬性應匯出到 JSON。此用例類似於前一個用例,將屬性標記為私人。但它也有所不同,因為這更多與匯出有關,而且適用於稍微不同的考量。例如:物件可以從 JSON 完全重建嗎?
作為可列舉性的替代方案,物件可以實作方法 .toJSON()
,而 JSON.stringify()
會字串化該方法傳回的內容,而不是物件本身。以下範例說明了它的運作方式。
class Point {
static fromJSON(json) {
return new Point(json[0], json[1]);
}
constructor(x, y) {
this.x = x;
this.y = y;
}
toJSON() {
return [this.x, this.y];
}
}
assert.equal(
JSON.stringify(new Point(8, -3)),
'[8,-3]'
);
我發現 toJSON()
比可列舉性更簡潔。它也給予我們更多自由度,讓我們可以決定儲存格式應該是什麼樣子。
我們已經看到,幾乎所有非可列舉性的應用程式都是權宜之計,現在有其他更好的解決方案。
對於我們自己的程式碼,我們通常可以假裝可列舉性不存在
也就是說,我們會自動遵循最佳實務。