Object.create()
:透過描述符建立物件Object.getOwnPropertyDescriptors()
的使用案例
.length
在本章中,我們將更深入地了解 ECMAScript 規範如何看待 JavaScript 物件。特別是,屬性在規範中並非原子性的,而是由多個屬性組成(想想記錄中的欄位)。甚至資料屬性的值都儲存在屬性中!
在 ECMAScript 規範中,物件包含
規範將內部插槽描述如下。我加入了項目符號並強調了一部分
undefined
。[[ ]]
括起來的名稱來識別內部方法和內部插槽。有兩種內部插槽
一般物件具有下列資料插槽
.[[Prototype]]: null | 物件
Object.getPrototypeOf()
和 Object.setPrototypeOf()
間接存取。.[[Extensible]]: 布林值
Object.preventExtensions()
設定為 false
。.[[PrivateFieldValues]]: EntryList
屬性的鍵為下列其中一者
有兩種屬性,它們的屬性決定其特性
value
儲存任何 JavaScript 值。get
中,後者儲存在屬性 set
中。此外,還有兩種屬性都具備。下表列出所有屬性及其預設值。
屬性種類 | 屬性名稱和類型 | 預設值 |
---|---|---|
資料屬性 | value: any |
undefined |
writable: boolean |
false |
|
存取器屬性 | get: (this: any) => any |
undefined |
set: (this: any, v: any) => void |
undefined |
|
所有屬性 | configurable: boolean |
false |
enumerable: boolean |
false |
我們已經看過屬性 value
、get
和 set
。其他屬性的運作方式如下
writable
決定資料屬性的值是否可以變更。configurable
決定屬性的屬性是否可以變更。如果為 false
,則
value
。writable
從 true
變更為 false
。這種異常現象的背後原因 源自歷史:陣列的屬性 .length
一直都是可寫入且不可設定的。允許變更其 writable
屬性,讓我們得以凍結陣列。enumerable
會影響某些操作(例如 Object.keys()
)。如果為 false
,則這些操作會忽略屬性。大多數屬性都是可列舉的(例如透過指派或物件文字建立的屬性),這就是為什麼你很少會在實務中注意到這個屬性的原因。如果你仍然有興趣了解它的運作方式,請參閱 §12「屬性的可列舉性」。如果繼承而來的屬性不可寫入,我們無法使用指派來建立具有相同鍵的自己的屬性
const proto = {
prop: 1,
};
// Make proto.prop non-writable:
Object.defineProperty(
proto, 'prop', {writable: false});
const obj = Object.create(proto);
assert.throws(
() => obj.prop = 2,
/^TypeError: Cannot assign to read only property 'prop'/);
如需更多資訊,請參閱 §11.3.4「繼承而來的唯讀屬性會阻止透過指派建立自己的屬性」。
屬性描述子將屬性的屬性編碼為 JavaScript 物件。其 TypeScript 介面如下所示。
interface DataPropertyDescriptor {
value?: any;
writable?: boolean;
configurable?: boolean;
enumerable?: boolean;
}
interface AccessorPropertyDescriptor {
get?: (this: any) => any;
set?: (this: any, v: any) => void;
configurable?: boolean;
enumerable?: boolean;
}
type PropertyDescriptor = DataPropertyDescriptor | AccessorPropertyDescriptor;
問號表示所有屬性都是選用的。§9.7 “省略描述子屬性” 說明如果省略這些屬性會發生什麼事。
Object.getOwnPropertyDescriptor()
:擷取單一屬性的描述子考慮下列物件
const legoBrick = {
kind: 'Plate 1x3',
color: 'yellow',
get description() {
return `${this.kind} (${this.color})`;
},
};
我們先為資料屬性 .color
取得描述子
assert.deepEqual(
Object.getOwnPropertyDescriptor(legoBrick, 'color'),
{
value: 'yellow',
writable: true,
enumerable: true,
configurable: true,
});
存取器屬性 .description
的描述子如下所示
const desc = Object.getOwnPropertyDescriptor.bind(Object);
assert.deepEqual(
Object.getOwnPropertyDescriptor(legoBrick, 'description'),
{
get: desc(legoBrick, 'description').get, // (A)
set: undefined,
enumerable: true,
configurable: true
});
使用 A 行的公用函式 desc()
可確保 .deepEqual()
正常運作。
Object.getOwnPropertyDescriptors()
:擷取物件所有屬性的描述子const legoBrick = {
kind: 'Plate 1x3',
color: 'yellow',
get description() {
return `${this.kind} (${this.color})`;
},
};
const desc = Object.getOwnPropertyDescriptor.bind(Object);
assert.deepEqual(
Object.getOwnPropertyDescriptors(legoBrick),
{
kind: {
value: 'Plate 1x3',
writable: true,
enumerable: true,
configurable: true,
},
color: {
value: 'yellow',
writable: true,
enumerable: true,
configurable: true,
},
description: {
get: desc(legoBrick, 'description').get, // (A)
set: undefined,
enumerable: true,
configurable: true,
},
});
使用 A 行的輔助函式 desc()
可確保 .deepEqual()
正常運作。
如果我們透過屬性描述子 propDesc
定義具有金鑰 k
的屬性,會發生下列情況
k
的屬性,會建立一個新的自有屬性,其屬性由 propDesc
指定。k
的屬性,定義會變更屬性的屬性,使其符合 propDesc
。Object.defineProperty()
:透過描述子定義單一屬性首先,我們透過描述子建立一個新屬性
const car = {};
Object.defineProperty(car, 'color', {
value: 'blue',
writable: true,
enumerable: true,
configurable: true,
});
assert.deepEqual(
car,
{
color: 'blue',
});
接下來,我們透過描述子變更屬性的種類;我們將資料屬性轉換為 getter
const car = {
color: 'blue',
};
let readCount = 0;
Object.defineProperty(car, 'color', {
get() {
readCount++;
return 'red';
},
});
assert.equal(car.color, 'red');
assert.equal(readCount, 1);
最後,我們透過描述子變更資料屬性的值
const car = {
color: 'blue',
};
// Use the same attributes as assignment:
Object.defineProperty(
car, 'color', {
value: 'green',
writable: true,
enumerable: true,
configurable: true,
});
assert.deepEqual(
car,
{
color: 'green',
});
我們使用與指定相同屬性屬性。
Object.defineProperties()
:透過描述子定義多個屬性Object.defineProperties()
是 `Object.defineProperty()
的多屬性版本
const legoBrick1 = {};
Object.defineProperties(
legoBrick1,
{
kind: {
value: 'Plate 1x3',
writable: true,
enumerable: true,
configurable: true,
},
color: {
value: 'yellow',
writable: true,
enumerable: true,
configurable: true,
},
description: {
get: function () {
return `${this.kind} (${this.color})`;
},
enumerable: true,
configurable: true,
},
});
assert.deepEqual(
legoBrick1,
{
kind: 'Plate 1x3',
color: 'yellow',
get description() {
return `${this.kind} (${this.color})`;
},
});
Object.create()
:透過描述子建立物件Object.create()
會建立一個新物件。其第一個引數指定該物件的原型。其第二個引數(選用)指定該物件屬性的描述子。在下列範例中,我們建立與前一個範例相同的物件。
const legoBrick2 = Object.create(
Object.prototype,
{
kind: {
value: 'Plate 1x3',
writable: true,
enumerable: true,
configurable: true,
},
color: {
value: 'yellow',
writable: true,
enumerable: true,
configurable: true,
},
description: {
get: function () {
return `${this.kind} (${this.color})`;
},
enumerable: true,
configurable: true,
},
});
// Did we really create the same object?
assert.deepEqual(legoBrick1, legoBrick2); // Yes!
Object.getOwnPropertyDescriptors()
的使用案例如果將 Object.getOwnPropertyDescriptors()
與 Object.defineProperties()
或 Object.create()
結合使用,它有助於我們處理兩個使用案例。
自 ES6 以來,JavaScript 已具備用於複製屬性的工具方法:Object.assign()
。不過,此方法使用簡單的取得和設定操作來複製其金鑰為 key
的屬性
這表示它只有在以下情況下才會建立屬性的忠實副本
writable
為 true
,且其屬性 enumerable
為 true
(因為這是指派建立屬性的方式)。下列範例說明了此限制。物件 source
有其金鑰為 data
的設定程式。
const source = {
set data(value) {
this._data = value;
}
};
// Property `data` exists because there is only a setter
// but has the value `undefined`.
assert.equal('data' in source, true);
assert.equal(source.data, undefined);
如果我們使用 Object.assign()
來複製屬性 data
,則存取器屬性 data
會轉換為資料屬性
const target1 = {};
Object.assign(target1, source);
assert.deepEqual(
Object.getOwnPropertyDescriptor(target1, 'data'),
{
value: undefined,
writable: true,
enumerable: true,
configurable: true,
});
// For comparison, the original:
const desc = Object.getOwnPropertyDescriptor.bind(Object);
assert.deepEqual(
Object.getOwnPropertyDescriptor(source, 'data'),
{
get: undefined,
set: desc(source, 'data').set,
enumerable: true,
configurable: true,
});
很幸運地,將 Object.getOwnPropertyDescriptors()
與 Object.defineProperties()
搭配使用,確實會複製屬性 data
const target2 = {};
Object.defineProperties(
target2, Object.getOwnPropertyDescriptors(source));
assert.deepEqual(
Object.getOwnPropertyDescriptor(target2, 'data'),
{
get: undefined,
set: desc(source, 'data').set,
enumerable: true,
configurable: true,
});
super
的方法使用 super
的方法與其家系物件(儲存該方法的物件)緊密連結。目前沒有辦法將此類方法複製或移動到不同的物件。
Object.getOwnPropertyDescriptors()
的用例:複製物件淺層複製類似於複製屬性,這就是為什麼 Object.getOwnPropertyDescriptors()
在此也是一個好選擇。
若要建立複製,我們使用 Object.create()
const original = {
set data(value) {
this._data = value;
}
};
const clone = Object.create(
Object.getPrototypeOf(original),
Object.getOwnPropertyDescriptors(original));
assert.deepEqual(original, clone);
如需有關此主題的更多資訊,請參閱 §6「複製物件和陣列」。
描述符的所有屬性都是選用的。省略屬性時會發生什麼事,取決於操作。
當我們透過描述符建立新屬性時,省略屬性表示使用其預設值
const car = {};
Object.defineProperty(
car, 'color', {
value: 'red',
});
assert.deepEqual(
Object.getOwnPropertyDescriptor(car, 'color'),
{
value: 'red',
writable: false,
enumerable: false,
configurable: false,
});
如果我們變更現有屬性,則省略描述符屬性表示不變更對應的屬性
const car = {
color: 'yellow',
};
assert.deepEqual(
Object.getOwnPropertyDescriptor(car, 'color'),
{
value: 'yellow',
writable: true,
enumerable: true,
configurable: true,
});
Object.defineProperty(
car, 'color', {
value: 'pink',
});
assert.deepEqual(
Object.getOwnPropertyDescriptor(car, 'color'),
{
value: 'pink',
writable: true,
enumerable: true,
configurable: true,
});
屬性屬性的通則(少數例外)為
原型鏈開頭的物件屬性通常具有可寫入、可列舉和可設定的特性。
如 可列舉性章節 所述,大多數繼承的屬性都是不可列舉的,以將它們隱藏起來,避免 for-in
迴圈等舊有建構式。繼承的屬性通常具有可寫入和可設定的特性。
const obj = {};
obj.prop = 3;
assert.deepEqual(
Object.getOwnPropertyDescriptors(obj),
{
prop: {
value: 3,
writable: true,
enumerable: true,
configurable: true,
}
});
const obj = { prop: 'yes' };
assert.deepEqual(
Object.getOwnPropertyDescriptors(obj),
{
prop: {
value: 'yes',
writable: true,
enumerable: true,
configurable: true
}
});
.length
陣列的自有屬性 .length
是不可列舉的,因此不會被 Object.assign()
、散布和其他類似操作複製。它也是不可設定的
> Object.getOwnPropertyDescriptor([], 'length')
{ value: 0, writable: true, enumerable: false, configurable: false }
> Object.getOwnPropertyDescriptor('abc', 'length')
{ value: 3, writable: false, enumerable: false, configurable: false }
.length
是一個特殊的資料屬性,它會受到其他自有屬性(特別是索引屬性)影響(並影響它們)。
assert.deepEqual(
Object.getOwnPropertyDescriptor(Array.prototype, 'map'),
{
value: Array.prototype.map,
writable: true,
enumerable: false,
configurable: true
});
class DataContainer {
accessCount = 0;
constructor(data) {
this.data = data;
}
getData() {
this.accessCount++;
return this.data;
}
}
assert.deepEqual(
Object.getOwnPropertyDescriptors(DataContainer.prototype),
{
constructor: {
value: DataContainer,
writable: true,
enumerable: false,
configurable: true,
},
getData: {
value: DataContainer.prototype.getData,
writable: true,
enumerable: false,
configurable: true,
}
});
請注意,DataContainer
實例的所有自有屬性都是可寫入、可列舉和可設定的
const dc = new DataContainer('abc')
assert.deepEqual(
Object.getOwnPropertyDescriptors(dc),
{
accessCount: {
value: 0,
writable: true,
enumerable: true,
configurable: true,
},
data: {
value: 'abc',
writable: true,
enumerable: true,
configurable: true,
}
});
下列工具方法使用屬性描述符
Object.defineProperty(obj: object, key: string|symbol, propDesc: PropertyDescriptor): object
[ES5]
在 obj
上建立或變更一個屬性,其金鑰為 key
,其屬性透過 propDesc
指定。傳回已修改的物件。
Object.defineProperties(obj: object, properties: {[k: string|symbol]: PropertyDescriptor}): object
[ES5]
Object.defineProperty()
的批次版本。properties
物件的每個屬性 p
指定一個要新增到 obj
的屬性:p
的金鑰指定屬性的金鑰,p
的值是一個描述符,指定屬性的屬性。
Object.create(proto: null|object, properties?: {[k: string|symbol]: PropertyDescriptor}): object
[ES5]
首先,建立一個原型為 proto
的物件。然後,如果已提供選用參數 properties
,則以與 Object.defineProperties()
相同的方式新增屬性。最後,傳回結果。例如,下列程式碼片段產生與前一個片段相同的結果
Object.getOwnPropertyDescriptor(obj: object, key: string|symbol): undefined|PropertyDescriptor
[ES5]
傳回 obj
的自有(非繼承)屬性的描述符,其金鑰為 key
。如果沒有此類屬性,則傳回 undefined
。
Object.getOwnPropertyDescriptors(obj: object): {[k: string|symbol]: PropertyDescriptor}
[ES2017]
傳回一個物件,其中 obj
的每個屬性金鑰 'k'
都對應到 obj.k
的屬性描述符。結果可用作 Object.defineProperties()
和 Object.create()
的輸入。
const propertyKey = Symbol('propertyKey');
const obj = {
[propertyKey]: 'abc',
get count() { return 123 },
};
const desc = Object.getOwnPropertyDescriptor.bind(Object);
assert.deepEqual(
Object.getOwnPropertyDescriptors(obj),
{
[propertyKey]: {
value: 'abc',
writable: true,
enumerable: true,
configurable: true
},
count: {
get: desc(obj, 'count').get, // (A)
set: undefined,
enumerable: true,
configurable: true
}
});
在 A 行中使用 desc()
是一種解決方法,讓 .deepEqual()
能夠運作。
接下來的三章節會提供更多關於屬性屬性的詳細資訊