...
) [ES2018]
this
this
.call()
.bind()
this
陷阱:提取方法this
陷阱:意外遮蔽 this
this
在各種情境下的值(進階)in
運算子:是否存在具有給定金鑰的屬性?Object.values()
列出屬性值Object.entries()
列出屬性條目 [ES2017]Object.fromEntries()
組合物件 [ES2019].toString()
.valueOf()
Object.assign()
[ES6]在本書中,JavaScript 的物件導向程式 (OOP) 風格分為四個步驟介紹。本章涵蓋步驟 1;下一章 涵蓋步驟 2-4。這些步驟如下(圖 8)
在 JavaScript 中
物件在 JavaScript 中扮演兩個角色
這些角色會影響物件在本章節中的說明方式
讓我們首先探討物件的記錄角色。
物件文字是建立物件作為記錄的一種方式。它們是 JavaScript 的突出特點:我們可以直接建立物件,無需類別!以下是一個範例
const jane = {
first: 'Jane',
last: 'Doe', // optional trailing comma
; }
在範例中,我們透過物件文字建立了一個物件,其以大括號 {}
開始和結束。在其中,我們定義了兩個屬性(鍵值對)
first
,值為 'Jane'
。last
,值為 'Doe'
。自 ES5 以來,物件文字中允許使用尾隨逗號。
我們稍後將看到指定屬性鍵的其他方式,但使用這種指定方式時,它們必須遵循 JavaScript 變數名稱的規則。例如,我們可以使用 first_name
作為屬性鍵,但不能使用 first-name
)。但是,允許使用保留字
const obj = {
if: true,
const: true,
; }
為了檢查各種操作對物件的影響,我們偶爾會在本章節的這一部分中使用 Object.keys()
。它列出屬性鍵
> Object.keys({a:1, b:2})[ 'a', 'b' ]
只要屬性的值是透過變數名稱定義的,而且該名稱與鍵相同,我們就可以省略該鍵。
function createPoint(x, y) {
return {x, y};
}.deepEqual(
assertcreatePoint(9, 2),
x: 9, y: 2 }
{ ; )
以下是我們取得(讀取)屬性的方式(A 行)
const jane = {
first: 'Jane',
last: 'Doe',
;
}
// Get property .first
.equal(jane.first, 'Jane'); // (A) assert
取得未知屬性會產生 undefined
.equal(jane.unknownProperty, undefined); assert
以下是我們設定(寫入)屬性的方式
const obj = {
prop: 1,
;
}.equal(obj.prop, 1);
assert.prop = 2; // (A)
obj.equal(obj.prop, 2); assert
我們剛剛透過設定變更了現有屬性。如果我們設定未知屬性,我們將建立一個新條目
const obj = {}; // empty object
.deepEqual(
assertObject.keys(obj), []);
.unknownProperty = 'abc';
obj.deepEqual(
assertObject.keys(obj), ['unknownProperty']);
以下程式碼顯示如何透過物件文字建立方法 .says()
const jane = {
first: 'Jane', // data property
says(text) { // method
return `${this.first} says “${text}”`; // (A)
, // comma as separator (optional at end)
};
}.equal(jane.says('hello'), 'Jane says “hello”'); assert
在方法呼叫 jane.says('hello')
期間,jane
被稱為方法呼叫的接收者,並指定給特殊變數 this
(有關 this
的更多資訊,請參閱 §28.4「方法和特殊變數 this
」)。這使方法 .says()
能夠在 A 行中存取同層屬性 .first
。
JavaScript 中有兩種存取器
取得器是透過在方法定義前加上修飾詞 get
來建立的
const jane = {
first: 'Jane',
last: 'Doe',
full() {
get return `${this.first} ${this.last}`;
,
};
}
.equal(jane.full, 'Jane Doe');
assert.first = 'John';
jane.equal(jane.full, 'John Doe'); assert
設定器是透過在方法定義前加上修飾詞 set
來建立的
const jane = {
first: 'Jane',
last: 'Doe',
full(fullName) {
set const parts = fullName.split(' ');
this.first = parts[0];
this.last = parts[1];
,
};
}
.full = 'Richard Roe';
jane.equal(jane.first, 'Richard');
assert.equal(jane.last, 'Roe'); assert
練習:透過物件文字建立物件
exercises/single-objects/color_point_object_test.mjs
...
)[ES2018]在函式呼叫內,擴散(...
)會將 可迭代 物件 的迭代值轉換為參數。
在物件文字內,擴散屬性會將另一個物件的屬性新增到目前的物件
> const obj = {foo: 1, bar: 2};
> {...obj, baz: 3}{ foo: 1, bar: 2, baz: 3 }
如果屬性金鑰發生衝突,最後提到的屬性會「獲勝」
> const obj = {foo: 1, bar: 2, baz: 3};
> {...obj, foo: true}{ foo: true, bar: 2, baz: 3 }
> {foo: true, ...obj}{ foo: 1, bar: 2, baz: 3 }
所有值都可以擴散,甚至包括 undefined
和 null
> {...undefined}{}
> {...null}{}
> {...123}{}
> {...'abc'}{ '0': 'a', '1': 'b', '2': 'c' }
> {...['a', 'b']}{ '0': 'a', '1': 'b' }
字串和陣列的屬性 .length
會隱藏在這種運算中(它不是 可列舉的;有關更多資訊,請參閱 §28.8.3「屬性屬性和屬性描述符 [ES5]」)。
我們可以使用擴散來建立物件 original
的副本
const copy = {...original};
注意事項 – 複製是淺層的:copy
是個新的物件,包含 original
所有屬性(金鑰值條目)的複製品。但是,如果屬性值是物件,則不會複製它們本身;它們會在 original
和 copy
之間共用。我們來看個範例
const original = { a: 1, b: {foo: true} };
const copy = {...original};
copy
的第一層真的是副本:如果我們變更該層級的任何屬性,它不會影響原始檔
.a = 2;
copy.deepEqual(
assert, { a: 1, b: {foo: true} }); // no change original
但是,更深層的層級不會被複製。例如,.b
的值會在 original 和 copy 之間共用。在 copy 中變更 .b
也會在 original 中變更它。
.b.foo = false;
copy.deepEqual(
assert, { a: 1, b: {foo: false} }); original
JavaScript 沒有內建支援深度複製
深度複製 物件(所有層級都會被複製)出了名的難以以通用方式執行。因此,JavaScript 目前沒有內建運算來執行它們。如果我們需要這種運算,我們必須自己實作。
如果我們程式碼的其中一個輸入是一個包含資料的物件,我們可以透過指定預設值來讓屬性變成可選的,這些預設值會在這些屬性不存在時使用。一種這樣做的方法是透過一個物件,其屬性包含預設值。在以下範例中,該物件是 DEFAULTS
const DEFAULTS = {foo: 'a', bar: 'b'};
const providedData = {foo: 1};
const allData = {...DEFAULTS, ...providedData};
.deepEqual(allData, {foo: 1, bar: 'b'}); assert
結果,物件 allData
是透過複製 DEFAULTS
並使用 providedData
的屬性覆寫其屬性而建立的。
但是我們不需要一個物件來指定預設值;我們也可以個別在物件字面上指定它們
const providedData = {foo: 1};
const allData = {foo: 'a', bar: 'b', ...providedData};
.deepEqual(allData, {foo: 1, bar: 'b'}); assert
到目前為止,我們已經遇到一種變更物件屬性 .foo
的方法:我們將其 設定(A 行)並變異物件。也就是說,這種變更屬性的方法是 破壞性的。
const obj = {foo: 'a', bar: 'b'};
.foo = 1; // (A)
obj.deepEqual(obj, {foo: 1, bar: 'b'}); assert
透過擴散,我們可以 非破壞性 地變更 .foo
– 我們製作一個 obj
的副本,其中 .foo
有不同的值
const obj = {foo: 'a', bar: 'b'};
const updatedObj = {...obj, foo: 1};
.deepEqual(updatedObj, {foo: 1, bar: 'b'}); assert
練習:透過擴散非破壞性地更新屬性(固定金鑰)
exercises/single-objects/update_name_test.mjs
this
讓我們重新檢視用於介紹方法的範例
const jane = {
first: 'Jane',
says(text) {
return `${this.first} says “${text}”`;
,
}; }
有點令人驚訝的是,方法是函式
.equal(typeof jane.says, 'function'); assert
為什麼會這樣?我們在 可呼叫值章節 中學到,一般函式扮演多種角色。方法 是其中一種角色。因此,在底層,jane
大致如下所示。
const jane = {
first: 'Jane',
says: function (text) {
return `${this.first} says “${text}”`;
,
}; }
this
考慮以下程式碼
const obj = {
someMethod(x, y) {
.equal(this, obj); // (A)
assert.equal(x, 'a');
assert.equal(y, 'b');
assert
};
}.someMethod('a', 'b'); // (B) obj
在 B 行中,obj
是方法呼叫的 接收者。它透過一個隱含(隱藏)參數傳遞給儲存在 obj.someMethod
中的函式,其名稱為 this
(A 行)。
這一點很重要:瞭解 this
的最佳方式是將其視為一般函式(因此也包括方法)的隱含參數。
.call()
方法是函式,而在 §25.7 「函式方法:.call()
、.apply()
、.bind()
」 中,我們看到函式本身有方法。其中一種方法是 .call()
。讓我們看一個範例來瞭解這個方法如何運作。
在前一章節中,有這個方法呼叫
.someMethod('a', 'b') obj
此呼叫等同於
.someMethod.call(obj, 'a', 'b'); obj
也等同於
const func = obj.someMethod;
.call(obj, 'a', 'b'); func
.call()
使通常隱含的參數 this
明確化:透過 .call()
呼叫函式時,第一個參數是 this
,接著是常規(明確)函式參數。
順帶一提,這表示實際上共有兩個不同的點運算子
obj.prop
obj.prop()
它們的不同之處在於 (2) 不只是 (1) 後面加上函式呼叫運算子 ()
。相反地,(2) 另外提供一個 this
值。
.bind()
.bind()
是函式物件的另一種方法。在以下程式碼中,我們使用 .bind()
將方法 .says()
轉換為獨立函式 func()
const jane = {
first: 'Jane',
says(text) {
return `${this.first} says “${text}”`; // (A)
,
};
}
const func = jane.says.bind(jane, 'hello');
.equal(func(), 'Jane says “hello”'); assert
在此透過 .bind()
將 this
設定為 jane
至關重要。否則,func()
無法正常運作,因為 this
用於 A 行。在下一章節中,我們將探討原因。
this
陷阱:擷取方法我們現在對函式和方法相當了解,準備來看看涉及方法和 this
的最大陷阱:如果我們不小心,函式呼叫從物件擷取的方法可能會失敗。
在以下範例中,我們在擷取方法 jane.says()
、將其儲存在變數 func
中,以及函式呼叫 func()
時失敗。
const jane = {
first: 'Jane',
says(text) {
return `${this.first} says “${text}”`;
,
};
}const func = jane.says; // extract the method
.throws(
assert=> func('hello'), // (A)
()
{name: 'TypeError',
message: "Cannot read property 'first' of undefined",
; })
在 A 行中,我們進行一般函式呼叫。而在一般函式呼叫中,this
是 undefined
(如果 嚴格模式 處於啟用狀態,而它幾乎總是啟用的)。因此,A 行等同於
.throws(
assert=> jane.says.call(undefined, 'hello'), // `this` is undefined!
()
{name: 'TypeError',
message: "Cannot read property 'first' of undefined",
; })
我們要如何修正這個問題?我們需要使用 .bind()
來擷取方法 .says()
const func2 = jane.says.bind(jane);
.equal(func2('hello'), 'Jane says “hello”'); assert
.bind()
確保當我們呼叫 func()
時,this
永遠是 jane
。
我們也可以使用箭頭函式來擷取方法
const func3 = text => jane.says(text);
.equal(func3('hello'), 'Jane says “hello”'); assert
以下是我們可能在實際網頁開發中看到的程式碼的簡化版本
class ClickHandler {
constructor(id, elem) {
this.id = id;
.addEventListener('click', this.handleClick); // (A)
elem
}handleClick(event) {
alert('Clicked ' + this.id);
} }
在 A 行中,我們沒有正確擷取方法 .handleClick()
。我們應該這樣做
.addEventListener('click', this.handleClick.bind(this)); elem
唉,沒有簡單的方法可以避免擷取方法的陷阱:每當我們擷取方法時,我們都必須小心並正確執行此操作,例如,透過繫結 this
或使用箭頭函式。
練習:擷取方法
練習/單一物件/method_extraction_exrc.mjs
this
陷阱:意外遮蔽 this
意外遮蔽
this
僅是普通函式的問題
箭頭函式不會遮蔽 this
。
考慮以下問題:當我們在普通函式內部時,我們無法存取周圍範圍的 this
,因為普通函式有自己的 this
。換句話說,內層範圍中的變數會隱藏外層範圍中的變數。這稱為 遮蔽。以下程式碼是一個範例
const prefixer = {
prefix: '==> ',
prefixStringArray(stringArray) {
return stringArray.map(
function (x) {
return this.prefix + x; // (A)
;
}),
};
}.throws(
assert=> prefixer.prefixStringArray(['a', 'b']),
() /^TypeError: Cannot read property 'prefix' of undefined$/);
在 A 行中,我們想要存取 .prefixStringArray()
的 this
。但我們無法存取,因為周圍的普通函式有自己的 this
,會遮蔽(封鎖存取)方法的 this
。前一個 this
的值為 undefined
,因為回呼是函式呼叫的。這說明了錯誤訊息。
解決此問題最簡單的方法是透過箭頭函式,它沒有自己的 this
,因此不會遮蔽任何東西
const prefixer = {
prefix: '==> ',
prefixStringArray(stringArray) {
return stringArray.map(
=> {
(x) return this.prefix + x;
;
}),
};
}.deepEqual(
assert.prefixStringArray(['a', 'b']),
prefixer'==> a', '==> b']); [
我們也可以將 this
儲存在不同的變數中(A 行),這樣它就不會被遮蔽
prefixStringArray(stringArray) {
const that = this; // (A)
return stringArray.map(
function (x) {
return that.prefix + x;
;
}), }
另一個選項是透過 .bind()
為回呼指定固定的 this
(A 行)
prefixStringArray(stringArray) {
return stringArray.map(
function (x) {
return this.prefix + x;
.bind(this)); // (A)
}, }
最後,.map()
讓我們可以為 this
指定一個值(A 行),它在呼叫回呼時會使用該值
prefixStringArray(stringArray) {
return stringArray.map(
function (x) {
return this.prefix + x;
,
}this); // (A)
, }
this
的陷阱如果您遵循 §25.3.4「建議:優先使用專門函式而非普通函式」 中的建議,您可以避免意外遮蔽 this
的陷阱。以下是摘要
使用箭頭函式作為匿名內聯函式。它們沒有 this
作為隱含參數,也不會遮蔽它。
對於命名獨立函式宣告,您可以使用箭頭函式或函式宣告。如果您使用後者,請確保在函式主體中未提及 this
。
this
在各種情境中的值(進階)this
在各種情境中的值為何?
在可呼叫實體內,this
的值取決於可呼叫實體如何被呼叫以及它是哪種類型的可呼叫實體
this === undefined
(在 嚴格模式 中)this
與周圍範圍中相同(詞彙 this
)this
是呼叫的接收者new
:this
參照新建立的執行個體我們也可以在所有常見頂層範圍中存取 this
<script>
元素:this === globalThis
this === undefined
this === module.exports
提示:假裝
this
在頂層範圍中不存在
我喜歡這麼做,因為頂層 this
很混淆,而且很少有用。
存在下列種類的選擇性鏈接操作
?.prop // optional static property access
obj?.[«expr»] // optional dynamic property access
obj?.(«arg0», «arg1») // optional function or method call func
大致概念是
undefined
也不是 null
,則執行問號之後的操作。undefined
。考慮下列資料
const persons = [
{surname: 'Zoe',
address: {
street: {
name: 'Sesame Street',
number: '123',
,
},
},
}
{surname: 'Mariner',
,
}
{surname: 'Carmen',
address: {
,
},
}; ]
我們可以使用選擇性鏈接安全地萃取街道名稱
const streetNames = persons.map(
=> p.address?.street?.name);
p .deepEqual(
assert, ['Sesame Street', undefined, undefined]
streetNames; )
nullish 合併運算子 讓我們可以使用預設值 '(no street)'
,而不是 undefined
const streetNames = persons.map(
=> p.address?.street?.name ?? '(no name)');
p .deepEqual(
assert, ['Sesame Street', '(no name)', '(no name)']
streetNames; )
下列兩個表達式是等效的
?.prop
o!== undefined && o !== null) ? o.prop : undefined (o
範例
.equal(undefined?.prop, undefined);
assert.equal(null?.prop, undefined);
assert.equal({prop:1}?.prop, 1); assert
下列兩個表達式是等效的
?.[«expr»]
o!== undefined && o !== null) ? o[«expr»] : undefined (o
範例
const key = 'prop';
.equal(undefined?.[key], undefined);
assert.equal(null?.[key], undefined);
assert.equal({prop:1}?.[key], 1); assert
下列兩個表達式是等效的
?.(arg0, arg1)
f!== undefined && f !== null) ? f(arg0, arg1) : undefined (f
範例
.equal(undefined?.(123), undefined);
assert.equal(null?.(123), undefined);
assert.equal(String?.(123), '123'); assert
請注意,如果此運算子的左手邊不可呼叫,則會產生錯誤
.throws(
assert=> true?.(123),
() TypeError);
為什麼?概念是,此運算子只容忍故意的遺漏。不可呼叫的值(除了 undefined
和 null
)可能是錯誤,而且應該報告,而不是解決。
在屬性存取和函式/方法呼叫的鏈中,一旦第一個選擇性運算子在左手邊遇到 undefined
或 null
,評估就會停止
function isInvoked(obj) {
let invoked = false;
?.a.b.m(invoked = true);
objreturn invoked;
}
.equal(
assertisInvoked({a: {b: {m() {}}}}), true);
// The left-hand side of ?. is undefined
// and the assignment is not executed
.equal(
assertisInvoked(undefined), false);
此行為不同於一般的運算子/函式,其中 JavaScript 總是在評估運算子/函式之前評估所有運算元/引數。這稱為短路。其他短路運算子
a && b
a || b
c ? t : e
o?.[x]
和 f?.()
中有句點?下列兩個選擇性運算子的語法並非理想
?.[«expr»] // better: obj?[«expr»]
obj?.(«arg0», «arg1») // better: func?(«arg0», «arg1») func
唉,較不優雅的語法是必要的,因為區分理想語法(第一個表達式)和條件運算子(第二個表達式)過於複雜
?['a', 'b', 'c'].map(x => x+x)
obj? ['a', 'b', 'c'].map(x => x+x) : [] obj
null?.prop
會評估為 undefined
,而不是 null
?運算子 ?.
主要與其右手邊有關:屬性 .prop
是否存在?如果不存在,則提早停止。因此,保留有關其左手邊的資訊很少有用。但是,只有一個「提早終止」值確實簡化了事情。
物件最適合用作記錄。但在 ES6 之前,JavaScript 沒有字典的資料結構(ES6 帶來了 Map)。因此,物件必須用作字典,這施加了一個重要的限制:金鑰必須是字串(ES6 也引入了符號)。
我們首先檢視與字典相關,但對於物件作為記錄也很有用的物件特徵。本節最後會提供實際使用物件作為字典的提示(劇透:如果可以,請使用 Maps)。
到目前為止,我們一直將物件用作記錄。屬性金鑰是必須為有效識別碼且在內部會變成字串的固定代碼
const obj = {
mustBeAnIdentifier: 123,
;
}
// Get property
.equal(obj.mustBeAnIdentifier, 123);
assert
// Set property
.mustBeAnIdentifier = 'abc';
obj.equal(obj.mustBeAnIdentifier, 'abc'); assert
接下來,我們將超越屬性金鑰的這個限制:在本節中,我們將使用任意固定字串作為金鑰。在下一個小節中,我們將動態計算金鑰。
有兩種技術可讓我們使用任意字串作為屬性金鑰。
首先,在透過物件文字建立屬性金鑰時,我們可以引用屬性金鑰(使用單引號或雙引號)
const obj = {
'Can be any string!': 123,
; }
其次,在取得或設定屬性時,我們可以在其中使用方括號和字串
// Get property
.equal(obj['Can be any string!'], 123);
assert
// Set property
'Can be any string!'] = 'abc';
obj[.equal(obj['Can be any string!'], 'abc'); assert
我們也可以將這些技術用於方法
const obj = {
'A nice method'() {
return 'Yes!';
,
};
}
.equal(obj['A nice method'](), 'Yes!'); assert
到目前為止,屬性金鑰始終是物件文字中的固定字串。在本節中,我們將學習如何動態計算屬性金鑰。這讓我們可以使用任意字串或符號。
物件文字中動態計算屬性金鑰的語法靈感來自動態存取屬性。也就是說,我們可以使用方括號來包裝表達式
const obj = {
'Hello world!']: true,
['f'+'o'+'o']: 123,
[Symbol.toStringTag]: 'Goodbye', // (A)
[;
}
.equal(obj['Hello world!'], true);
assert.equal(obj.foo, 123);
assert.equal(obj[Symbol.toStringTag], 'Goodbye'); assert
計算金鑰的主要使用案例是將符號作為屬性金鑰(A 行)。
請注意,用於取得和設定屬性的方括號運算子適用於任意表達式
.equal(obj['f'+'o'+'o'], 123);
assert.equal(obj['==> foo'.slice(-3)], 123); assert
方法也可以有計算屬性金鑰
const methodKey = Symbol();
const obj = {
[methodKey]() {return 'Yes!';
,
};
}
.equal(obj[methodKey](), 'Yes!'); assert
在本章的其餘部分,我們將再次主要使用固定屬性金鑰(因為它們在語法上更方便)。但所有功能也適用於任意字串和符號。
練習:透過擴散(計算金鑰)非破壞性地更新屬性
exercises/single-objects/update_property_test.mjs
in
運算子:是否存在具有給定金鑰的屬性?in
運算子檢查物件是否具有具有給定金鑰的屬性
const obj = {
foo: 'abc',
bar: false,
;
}
.equal('foo' in obj, true);
assert.equal('unknownKey' in obj, false); assert
我們也可以使用真值檢查來判斷屬性是否存在
.equal(
assert.foo ? 'exists' : 'does not exist',
obj'exists');
.equal(
assert.unknownKey ? 'exists' : 'does not exist',
obj'does not exist');
前述檢查之所以有效,是因為 obj.foo
為真值,而且讀取不存在的屬性會傳回 undefined
(為假值)。
不過,有一個重要的警告:如果屬性存在,但具有假值(undefined
、null
、false
、0
、""
等),真值檢查將會失敗。
.equal(
assert.bar ? 'exists' : 'does not exist',
obj'does not exist'); // should be: 'exists'
我們可以使用 delete
算子來刪除屬性
const obj = {
foo: 123,
;
}.deepEqual(Object.keys(obj), ['foo']);
assert
delete obj.foo;
.deepEqual(Object.keys(obj), []); assert
可列舉 | 不可列舉 | 字串 | 符號 | |
---|---|---|---|---|
Object.keys() |
✔ |
✔ |
||
Object.getOwnPropertyNames() |
✔ |
✔ |
✔ |
|
Object.getOwnPropertySymbols() |
✔ |
✔ |
✔ |
|
Reflect.ownKeys() |
✔ |
✔ |
✔ |
✔ |
表 19 中的每個方法都會傳回一個陣列,其中包含參數的自有屬性鍵。從這些方法的名稱中,我們可以看到以下區別
下一個區段會說明可列舉這個術語,並示範每個方法。
可列舉性是屬性的屬性。某些操作會忽略不可列舉的屬性,例如 Object.keys()
(請參閱表 19)和擴散屬性。預設情況下,大部分屬性都是可列舉的。以下範例顯示如何變更此設定。它也示範了列出屬性鍵的各種方式。
const enumerableSymbolKey = Symbol('enumerableSymbolKey');
const nonEnumSymbolKey = Symbol('nonEnumSymbolKey');
// We create enumerable properties via an object literal
const obj = {
enumerableStringKey: 1,
: 2,
[enumerableSymbolKey]
}
// For non-enumerable properties, we need a more powerful tool
Object.defineProperties(obj, {
nonEnumStringKey: {
value: 3,
enumerable: false,
,
}: {
[nonEnumSymbolKey]value: 4,
enumerable: false,
,
};
})
.deepEqual(
assertObject.keys(obj),
'enumerableStringKey' ]);
[ .deepEqual(
assertObject.getOwnPropertyNames(obj),
'enumerableStringKey', 'nonEnumStringKey' ]);
[ .deepEqual(
assertObject.getOwnPropertySymbols(obj),
, nonEnumSymbolKey ]);
[ enumerableSymbolKey.deepEqual(
assertReflect.ownKeys(obj),
['enumerableStringKey', 'nonEnumStringKey',
, nonEnumSymbolKey,
enumerableSymbolKey; ])
Object.defineProperties()
會在本章稍後說明。
Object.values()
列出屬性值Object.values()
會列出物件所有可列舉屬性的值
const obj = {foo: 1, bar: 2};
.deepEqual(
assertObject.values(obj),
1, 2]); [
Object.entries()
[ES2017] 列出屬性項目Object.entries()
會列出可列舉屬性的鍵值配對。每個配對都編碼為一個兩元素陣列
const obj = {foo: 1, bar: 2};
.deepEqual(
assertObject.entries(obj),
['foo', 1],
['bar', 2],
[; ])
練習:
Object.entries()
exercises/single-objects/find_key_test.mjs
物件的自有(非繼承)屬性總是會以以下順序列出
以下範例說明屬性金鑰如何根據這些規則進行排序
> Object.keys({b:0,a:0, 10:0,2:0})[ '2', '10', 'b', 'a' ]
屬性的順序
ECMAScript 規格 更詳細地描述屬性的順序。
Object.fromEntries()
組合物件 [ES2019]給定一個 [金鑰、值] 成對的 iterable,Object.fromEntries()
會建立一個物件
.deepEqual(
assertObject.fromEntries([['foo',1], ['bar',2]]),
{foo: 1,
bar: 2,
}; )
Object.fromEntries()
的作用與 Object.entries()
相反。
為了示範兩者,我們會在 下一個小節 中使用它們來實作 Underscore 函式庫中的兩個工具函式。
pick(object, ...keys)
pick
會傳回 object
的一份拷貝,其中只包含金鑰有在引數中提及的那些屬性
const address = {
street: 'Evergreen Terrace',
number: '742',
city: 'Springfield',
state: 'NT',
zip: '49007',
;
}.deepEqual(
assertpick(address, 'street', 'number'),
{street: 'Evergreen Terrace',
number: '742',
}; )
我們可以這樣實作 pick()
function pick(object, ...keys) {
const filteredEntries = Object.entries(object)
.filter(([key, _value]) => keys.includes(key));
return Object.fromEntries(filteredEntries);
}
invert(object)
invert
會傳回 object
的一份拷貝,其中所有屬性的金鑰和值互換
.deepEqual(
assertinvert({a: 1, b: 2, c: 3}),
1: 'a', 2: 'b', 3: 'c'}
{; )
我們可以這樣實作 invert()
function invert(object) {
const reversedEntries = Object.entries(object)
.map(([key, value]) => [value, key]);
return Object.fromEntries(reversedEntries);
}
Object.fromEntries()
的簡化實作以下函式是 Object.fromEntries()
的簡化版本
function fromEntries(iterable) {
const result = {};
for (const [key, value] of iterable) {
let coercedKey;
if (typeof key === 'string' || typeof key === 'symbol') {
= key;
coercedKey else {
} = String(key);
coercedKey
}= value;
result[coercedKey]
}return result;
}
練習:
Object.entries()
和 Object.fromEntries()
exercises/single-objects/omit_properties_test.mjs
如果我們將一般物件(透過物件文字建立)當成字典使用,我們必須注意兩個陷阱。
第一個陷阱是 in
運算子也會找到繼承的屬性
const dict = {};
.equal('toString' in dict, true); assert
我們希望將 dict
視為空物件,但 in
運算子會偵測到它從原型 Object.prototype
繼承的屬性。
第二個陷阱是我們無法使用屬性金鑰 __proto__
,因為它具有特殊功能(會設定物件的原型)
const dict = {};
'__proto__'] = 123;
dict[// No property was added to dict:
.deepEqual(Object.keys(dict), []); assert
那麼,如何避免這兩個陷阱?
以下程式碼示範如何將沒有原型的物件當成字典使用
const dict = Object.create(null); // no prototype
.equal('toString' in dict, false); // (A)
assert
'__proto__'] = 123;
dict[.deepEqual(Object.keys(dict), ['__proto__']); assert
我們避免了這兩個陷阱
__proto__
是透過 Object.prototype
實作的。這表示如果 Object.prototype
不在原型鏈中,它就會被關閉。 練習:使用物件作為字典
exercises/single-objects/simple_dict_test.mjs
Object.prototype
定義了幾個標準方法,可以覆寫這些方法來設定物件在語言中的處理方式。兩個重要的標準方法是
.toString()
.valueOf()
.toString()
.toString()
決定如何將物件轉換為字串
> String({toString() { return 'Hello!' }})'Hello!'
> String({})'[object Object]'
.valueOf()
.valueOf()
決定如何將物件轉換為數字
> Number({valueOf() { return 123 }})123
> Number({})NaN
以下小節簡要概述幾個進階主題。
Object.assign()
[ES6]Object.assign()
是一個工具方法
Object.assign(target, source_1, source_2, ···)
這個表達式會將 source_1
的所有屬性指定給 target
,然後將 source_2
的所有屬性指定給 target
,依此類推。最後,它會傳回 target
,例如
const target = { foo: 1 };
const result = Object.assign(
,
targetbar: 2},
{baz: 3, bar: 4});
{
.deepEqual(
assert, { foo: 1, bar: 4, baz: 3 });
result// target was modified and returned:
.equal(result, target); assert
Object.assign()
的使用案例類似於展開屬性。在某種程度上,它會以破壞性的方式展開。
Object.freeze(obj)
會讓 obj
完全不可變:我們無法變更屬性、新增屬性或變更其原型,例如
const frozen = Object.freeze({ x: 2, y: 5 });
.throws(
assert=> { frozen.x = 7 },
()
{name: 'TypeError',
message: /^Cannot assign to read only property 'x'/,
; })
有一個注意事項:Object.freeze(obj)
會淺層凍結。也就是說,只有 obj
的屬性會被凍結,但儲存在屬性中的物件不會被凍結。
更多資訊
有關凍結和其他鎖定物件方式的更多資訊,請參閱 深入探討 JavaScript。
就像物件是由屬性組成的一樣,屬性是由屬性組成的。屬性的值只是幾個屬性之一。其他屬性包括
writable
:是否可以變更屬性的值?enumerable
:屬性是否會被 Object.keys()
、展開等考慮在內?當我們使用其中一個操作來處理屬性時,屬性會透過屬性描述符指定:每個屬性代表一個屬性的物件。例如,這是我們如何讀取屬性 obj.foo
的屬性
const obj = { foo: 123 };
.deepEqual(
assertObject.getOwnPropertyDescriptor(obj, 'foo'),
{value: 123,
writable: true,
enumerable: true,
configurable: true,
; })
這是我們如何設定屬性 obj.bar
的屬性
const obj = {
foo: 1,
bar: 2,
;
}
.deepEqual(Object.keys(obj), ['foo', 'bar']);
assert
// Hide property `bar` from Object.keys()
Object.defineProperty(obj, 'bar', {
enumerable: false,
;
})
.deepEqual(Object.keys(obj), ['foo']); assert
進一步閱讀
測驗
請參閱 測驗應用程式。