...
) [ES2018]
Object.assign()
[ES6]this
this
.call()
.bind()
this
陷阱:萃取方法this
陷阱:意外遮蔽 this
this
在各種情境中的值 (進階)in
運算子:是否存在具有給定鍵的屬性?Object.keys()
等列出屬性金鑰Object.values()
列出屬性值Object.entries()
列出屬性條目 [ES2017]Object.fromEntries()
組合物件 [ES2019]Object.hasOwn()
:給定的屬性是否為自有(非繼承)屬性?[ES2022]本書中,JavaScript 的物件導向程式 (OOP) 風格分為四個步驟介紹。本章涵蓋步驟 1 和 2;下一章 涵蓋步驟 3 和 4。步驟如下(圖 8)
透過物件文字建立物件(以大括號開始和結束)
const myObject = { // object literal
myProperty: 1,
myMethod() {
return 2;
, // comma!
}myAccessor() {
get return this.myProperty;
, // comma!
}myAccessor(value) {
set this.myProperty = value;
, // last comma is optional
};
}
.equal(
assert.myProperty, 1
myObject;
).equal(
assert.myMethod(), 2
myObject;
).equal(
assert.myAccessor, 1
myObject;
).myAccessor = 3;
myObject.equal(
assert.myProperty, 3
myObject; )
能夠直接建立物件(不使用類別)是 JavaScript 的亮點之一。
散佈到物件中
const original = {
a: 1,
b: {
c: 3,
,
};
}
// Spreading (...) copies one object “into” another one:
const modifiedCopy = {
...original, // spreading
d: 4,
;
}
.deepEqual(
assert,
modifiedCopy
{a: 1,
b: {
c: 3,
,
}d: 4,
};
)
// Caveat: spreading copies shallowly (property values are shared)
.a = 5; // does not affect `original`
modifiedCopy.b.c = 6; // affects `original`
modifiedCopy.deepEqual(
assert,
original
{a: 1, // unchanged
b: {
c: 6, // changed
,
},
}; )
我們也可以使用散佈來建立物件的未修改(淺層)副本
const exactCopy = {...obj};
原型是 JavaScript 基本的繼承機制。即使是類別也是基於原型。每個物件都有 null
或物件作為其原型。後者物件也可以有原型,依此類推。一般來說,我們會得到原型的鏈。
原型像這樣管理
// `obj1` has no prototype (its prototype is `null`)
const obj1 = Object.create(null); // (A)
.equal(
assertObject.getPrototypeOf(obj1), null // (B)
;
)
// `obj2` has the prototype `proto`
const proto = {
protoProp: 'protoProp',
;
}const obj2 = {
__proto__: proto, // (C)
objProp: 'objProp',
}.equal(
assertObject.getPrototypeOf(obj2), proto
; )
注意事項
每個物件都繼承其原型的所有屬性
// `obj2` inherits .protoProp from `proto`
.equal(
assert.protoProp, 'protoProp'
obj2;
).deepEqual(
assertReflect.ownKeys(obj2),
'objProp'] // own properties of `obj2`
[; )
物件未繼承的屬性稱為其自有屬性。
原型最重要的使用案例是,多個物件可以透過繼承自共用原型的屬性來共用方法。
JavaScript 中的物件
在 JavaScript 中有兩種使用物件的方式
固定配置物件:使用這種方式,物件就像資料庫中的記錄。它們有固定數量的屬性,其鍵在開發時已知。它們的值通常有不同的類型。
const fixedLayoutObject = {
product: 'carrot',
quantity: 4,
; }
字典物件:使用這種方式,物件就像查詢表或映射。它們有可變數量的屬性,其鍵在開發時未知。它們的所有值都有相同的類型。
const dictionaryObject = {
'one']: 1,
['two']: 2,
[; }
請注意,這兩種方式也可以混合:有些物件既是固定配置物件,也是字典物件。
使用物件的方式會影響它們在本章中的說明方式
讓我們先探討固定配置物件。
物件文字是建立固定配置物件的一種方式。它們是 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}; // Same as: {x: x, y: 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
以下是我們設定(寫入)屬性的方式(A 行)
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', // value 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.5「方法和特殊變數 this
」)。這使方法 .says()
能夠在 A 行存取同層屬性 .first
。
存取器透過物件文字內部類似方法的語法定義:取得器和/或設定器(也就是說,每個存取器都有其中一個或兩個)。
呼叫存取器看起來就像存取值屬性
透過在方法定義前加上修飾詞 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/objects/color_point_object_test.mjs
...
) [ES2018]在物件文字中,散佈屬性會將另一個物件的屬性加入目前的物件
> const obj = {one: 1, two: 2};
> {...obj, three: 3}{ one: 1, two: 2, three: 3 }
const obj1 = {one: 1, two: 2};
const obj2 = {three: 3};
.deepEqual(
assert...obj1, ...obj2, four: 4},
{one: 1, two: 2, three: 3, four: 4}
{; )
如果屬性金鑰衝突,最後提到的屬性會「獲勝」
> const obj = {one: 1, two: 2, three: 3};
> {...obj, one: true}{ one: true, two: 2, three: 3 }
> {one: true, ...obj}{ one: 1, two: 2, three: 3 }
所有值都可以散佈,甚至 undefined
和 null
> {...undefined}{}
> {...null}{}
> {...123}{}
> {...'abc'}{ '0': 'a', '1': 'b', '2': 'c' }
> {...['a', 'b']}{ '0': 'a', '1': 'b' }
字串和陣列的屬性 .length
會隱藏在這種運算中(它不是可列舉的;請參閱 §28.8.1「屬性屬性和屬性描述符 [ES5]」 以取得更多資訊)。
散佈包含金鑰為符號的屬性(Object.keys()
、Object.values()
和 Object.entries()
會忽略這些屬性)
const symbolKey = Symbol('symbolKey');
const obj = {
stringKey: 1,
: 2,
[symbolKey];
}.deepEqual(
assert...obj, anotherStringKey: 3},
{
{stringKey: 1,
: 2,
[symbolKey]anotherStringKey: 3,
}; )
我們可以使用散佈來建立物件 original
的副本
const copy = {...original};
注意事項 – 複製是淺層的:copy
是個新的物件,包含 original
所有屬性(金鑰值條目)的副本。但如果屬性值是物件,則這些物件本身不會被複製;它們會在 original
和 copy
之間共用。我們來看一個範例
const original = { a: 1, b: {prop: true} };
const copy = {...original};
copy
的第一層真的是副本:如果我們變更該層級的任何屬性,它不會影響原始物件
.a = 2;
copy.deepEqual(
assert, { a: 1, b: {prop: true} }); // no change original
不過,較深層的層級不會被複製。例如,.b
的值會在原始物件和副本之間共用。在副本中變更 .b
也會在原始物件中變更它。
.b.prop = false;
copy.deepEqual(
assert, { a: 1, b: {prop: false} }); original
JavaScript 沒有內建支援深度複製
物件的深度複製(其中所有層級都會被複製)出了名的難以泛用地執行。因此,JavaScript 目前沒有內建的運算來執行深度複製。如果我們需要這種運算,我們必須自己實作。
如果我們程式碼的其中一個輸入是包含資料的物件,我們可以透過指定預設值(如果這些屬性遺失,就會使用這些值)來讓屬性變成選用的。執行此操作的一種技巧是透過包含預設值的物件。在以下範例中,該物件是 DEFAULTS
const DEFAULTS = {alpha: 'a', beta: 'b'};
const providedData = {alpha: 1};
const allData = {...DEFAULTS, ...providedData};
.deepEqual(allData, {alpha: 1, beta: 'b'}); assert
結果,物件 allData
,是透過複製 DEFAULTS
並用 providedData
的屬性覆寫其屬性建立的。
但我們不需要物件來指定預設值;我們也可以在物件文字內個別指定
const providedData = {alpha: 1};
const allData = {alpha: 'a', beta: 'b', ...providedData};
.deepEqual(allData, {alpha: 1, beta: 'b'}); assert
到目前為止,我們已經遇到一種變更物件屬性 .alpha
的方法:我們將它設定 (A 行) 並變異物件。也就是說,這種變更屬性的方法具有破壞性。
const obj = {alpha: 'a', beta: 'b'};
.alpha = 1; // (A)
obj.deepEqual(obj, {alpha: 1, beta: 'b'}); assert
透過散佈,我們可以非破壞性地變更 .alpha
– 我們製作一個 obj
的副本,其中 .alpha
具有不同的值
const obj = {alpha: 'a', beta: 'b'};
const updatedObj = {...obj, alpha: 1};
.deepEqual(updatedObj, {alpha: 1, beta: 'b'}); assert
練習:透過散佈非破壞性地更新屬性 (固定鍵)
exercises/objects/update_name_test.mjs
Object.assign()
[ES6]Object.assign()
是一種工具方法
Object.assign(target, source_1, source_2, ···)
此表達式會將 source_1
的所有屬性指定給 target
,然後將 source_2
的所有屬性指定給 target
,依此類推。最後,它傳回 target
– 例如
const target = { a: 1 };
const result = Object.assign(
,
targetb: 2},
{c: 3, b: true});
{
.deepEqual(
assert, { a: 1, b: true, c: 3 });
result// target was modified and returned:
.equal(result, target); assert
Object.assign()
的用例與散佈屬性類似。在某種程度上,它是以破壞性的方式散佈。
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
了解 this
的最佳方法是將它視為一般函式 (因此也包括方法) 的隱含參數。
.call()
方法是函式,而函式本身也有方法。其中一種方法是 .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 properties of undefined (reading 'first')",
; })
在 A 行中,我們進行一般函式呼叫。而在一般函式呼叫中,this
是 undefined
(如果嚴格模式處於啟用狀態,它幾乎總是處於啟用狀態)。因此,A 行等同於
.throws(
assert=> jane.says.call(undefined, 'hello'), // `this` is undefined!
()
{name: 'TypeError',
message: "Cannot read properties of undefined (reading 'first')",
}; )
我們如何修復這個問題?我們需要使用 .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()
。我們應該這樣做
const listener = this.handleClick.bind(this);
.addEventListener('click', listener);
elem
// Later, possibly:
.removeEventListener('click', listener); elem
每次呼叫 .bind()
都會建立一個新函式。這就是為什麼如果我們想要稍後移除它,就需要將結果儲存在某個地方。
唉,沒有簡單的方法可以避開擷取方法的陷阱:每當我們擷取方法時,我們都必須小心並正確執行 - 例如,透過繫結 this
或使用箭頭函式。
練習:擷取方法
exercises/objects/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']),
()
{name: 'TypeError',
message: "Cannot read properties of undefined (reading 'prefix')",
}; )
在 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 fixed property getting
obj?.[«expr»] // optional dynamic property getting
obj?.(«arg0», «arg1») // optional function or method call func
大致上的概念是
undefined
也不是 null
,則執行問號後面的運算。undefined
。三個語法的每個部分稍後會詳細說明。以下是一些最早的範例
> null?.propundefined
> {prop: 1}?.prop1
> null?.(123)undefined
> String?.(123)'123'
考慮以下資料
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; )
空值合併運算子 讓我們可以使用預設值 '(no name)'
取代 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 invokeM(value) {
return value?.a.b.m(); // (A)
}
const obj = {
a: {
b: {
m() { return 'result' }
}
};
}.equal(
assertinvokeM(obj), 'result'
;
).equal(
assertinvokeM(undefined), undefined // (B)
; )
考慮 B 行中的 invokeM(undefined)
:undefined?.a
是 undefined
。因此我們預期 A 行中的 .b
會失敗。但並非如此:?.
運算子遇到值 undefined
,整個表達式的評估會立即傳回 undefined
。
此行為與 JavaScript 在評估運算子之前總是評估所有運算元的正常運算子不同。這稱為短路。其他短路運算子為
(a && b)
:只有在 a
為真時才會評估 b
。(a || b)
:只有在 a
為假時才會評估 b
。(c ? t : e)
:如果 c
為真,則評估 t
。否則,評估 e
。選擇性鏈接也有一些缺點
選擇性鏈接的替代方案是在單一位置一次提取資訊
透過任一種方法,如果出現問題,都可以執行檢查並及早失敗。
進一步閱讀
?.
) 有什麼好的記憶法?你偶爾不確定選擇性鏈接運算子是以點號 (.?
) 還是問號 (?.
) 開頭嗎?那麼這個記憶法可能對你有幫助
?
) 左側不是 nullish.
) 存取屬性。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 也引入了符號)。
我們先來看看物件中與字典相關,但對固定配置物件也有用的功能。本節最後會提供實際使用物件作為字典的建議。(劇透:如果可以,最好使用 Map。)
到目前為止,我們一直使用固定配置物件。屬性鍵是固定的代號,必須是有效的識別碼,而且在內部會變成字串
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,
['p'+'r'+'o'+'p']: 123,
[Symbol.toStringTag]: 'Goodbye', // (A)
[;
}
.equal(obj['Hello world!'], true);
assert.equal(obj.prop, 123);
assert.equal(obj[Symbol.toStringTag], 'Goodbye'); assert
運算鍵的主要使用案例是將符號當作屬性鍵(A 行)。
請注意,用於取得和設定屬性的方括號運算子適用於任意運算式
.equal(obj['p'+'r'+'o'+'p'], 123);
assert.equal(obj['==> prop'.slice(4)], 123); assert
方法也可以有運算屬性鍵
const methodKey = Symbol();
const obj = {
[methodKey]() {return 'Yes!';
,
};
}
.equal(obj[methodKey](), 'Yes!'); assert
在本章的其餘部分,我們將再次主要使用固定的屬性鍵(因為它們在語法上更方便)。但所有功能也適用於任意字串和符號。
練習:透過展開(運算鍵)非破壞性地更新屬性
exercises/objects/update_property_test.mjs
in
運算子:是否存在具有給定金鑰的屬性?in
運算子檢查物件是否有具有給定金鑰的屬性
const obj = {
alpha: 'abc',
beta: false,
;
}
.equal('alpha' in obj, true);
assert.equal('beta' in obj, true);
assert.equal('unknownKey' in obj, false); assert
我們也可以使用真值檢查來判斷屬性是否存在
.equal(
assert.alpha ? 'exists' : 'does not exist',
obj'exists');
.equal(
assert.unknownKey ? 'exists' : 'does not exist',
obj'does not exist');
先前的檢查之所以有效,是因為 obj.alpha
為真值,而且讀取不存在的屬性會傳回 undefined
(為假值)。
不過,有一個重要的注意事項:如果屬性存在,但具有假值(undefined
、null
、false
、0
、""
等),真值檢查就會失敗。
.equal(
assert.beta ? 'exists' : 'does not exist',
obj'does not exist'); // should be: 'exists'
我們可以使用 delete
運算子刪除屬性
const obj = {
myProp: 123,
;
}
.deepEqual(Object.keys(obj), ['myProp']);
assertdelete obj.myProp;
.deepEqual(Object.keys(obj), []); assert
可列舉性 是屬性的 屬性。某些操作會忽略不可列舉的屬性,例如 Object.keys()
以及在擴散屬性時。預設情況下,大多數屬性都是可列舉的。以下範例說明如何變更這個設定,以及它如何影響擴散。
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,
,
};
})
// Non-enumerable properties are ignored by spreading:
.deepEqual(
assert...obj},
{
{enumerableStringKey: 1,
: 2,
[enumerableSymbolKey]
}; )
Object.defineProperties()
會在 本章節後續說明。下一個小節說明這些操作如何受到可列舉性的影響
Object.keys()
等方法列出屬性金鑰可列舉 | 不可列舉 | 字串 | 符號 | |
---|---|---|---|---|
Object.keys() |
✔ |
✔ |
||
Object.getOwnPropertyNames() |
✔ |
✔ |
✔ |
|
Object.getOwnPropertySymbols() |
✔ |
✔ |
✔ |
|
Reflect.ownKeys() |
✔ |
✔ |
✔ |
✔ |
表格 19 中的每個方法都會傳回一個陣列,其中包含參數的自有屬性金鑰。從這些方法的名稱中,我們可以看到以下區別
為了示範這四個操作,我們回顧一下前一個小節的範例
const enumerableSymbolKey = Symbol('enumerableSymbolKey');
const nonEnumSymbolKey = Symbol('nonEnumSymbolKey');
const obj = {
enumerableStringKey: 1,
: 2,
[enumerableSymbolKey]
}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.values()
列出屬性值Object.values()
會列出物件中所有可列舉字串金鑰屬性的值
const firstName = Symbol('firstName');
const obj = {
: 'Jane',
[firstName]lastName: 'Doe',
;
}.deepEqual(
assertObject.values(obj),
'Doe']); [
Object.entries()
列出屬性項目 [ES2017]Object.entries()
會將所有可列舉的字串金鑰屬性列出為金鑰值配對。每個配對都編碼為兩個元素的陣列
const firstName = Symbol('firstName');
const obj = {
: 'Jane',
[firstName]lastName: 'Doe',
;
}.deepEqual(
assertObject.entries(obj),
['lastName', 'Doe'],
[; ])
Object.entries()
的簡單實作以下函式是 Object.entries()
的簡化版本
function entries(obj) {
return Object.keys(obj)
.map(key => [key, obj[key]]);
}
練習:
Object.entries()
exercises/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()
會建立一個物件
const symbolKey = Symbol('symbolKey');
.deepEqual(
assertObject.fromEntries(
['stringKey', 1],
[, 2],
[symbolKey
],
)
{stringKey: 1,
: 2,
[symbolKey]
}; )
Object.fromEntries()
的作用與 Object.entries()
相反。不過,Object.entries()
會忽略符號金鑰屬性,而 Object.fromEntries()
卻不會(請參閱前一個範例)。
為了示範這兩者,我們將在 下一個小節 中使用它們來實作 Underscore 函式庫中的兩個工具函式。
pick()
Underscore 函式 pick()
有以下簽章
pick(object, ...keys)
它會傳回 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()
Underscore 函式 invert()
有以下簽章
invert(object)
它會傳回 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()
練習/物件/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); // prototype is `null`
.equal('toString' in dict, false); // (A)
assert
'__proto__'] = 123;
dict[.deepEqual(Object.keys(dict), ['__proto__']); assert
我們避免了兩個陷阱
__proto__
是透過 Object.prototype
實作的。這表示如果 Object.prototype
不在原型鏈中,它就會被關閉。 練習:使用物件作為字典
練習/物件/simple_dict_test.mjs
就像物件是由屬性組成,屬性是由屬性組成。屬性的值只是幾個屬性之一。其他屬性包括
writable
:是否可以變更屬性的值?enumerable
:屬性是否會被 Object.keys()
、展開等考慮?當我們使用其中一個處理屬性屬性的操作時,屬性會透過屬性描述符指定:每個屬性代表一個屬性的物件。例如,這是我們如何讀取屬性 obj.myProp
的屬性
const obj = { myProp: 123 };
.deepEqual(
assertObject.getOwnPropertyDescriptor(obj, 'myProp'),
{value: 123,
writable: true,
enumerable: true,
configurable: true,
; })
這是我們如何變更 obj.myProp
的屬性
.deepEqual(Object.keys(obj), ['myProp']);
assert
// Hide property `myProp` from Object.keys()
// by making it non-enumerable
Object.defineProperty(obj, 'myProp', {
enumerable: false,
;
})
.deepEqual(Object.keys(obj), []); assert
進一步閱讀
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()
會變更屬性和物件的特徵(例如,使其不可寫入和不可擴充,意即無法再新增任何屬性)。
有一個注意事項:Object.freeze(obj)
會淺層凍結。也就是說,只有 obj
的屬性會凍結,但儲存在屬性中的物件不會凍結。
更多資訊
有關凍結和其他鎖定物件方法的更多資訊,請參閱深入探討 JavaScript。
原型是 JavaScript 唯一的繼承機制:每個物件都有原型,可能是 null
或物件。在後者的情況下,物件會繼承原型中的所有屬性。
在物件文字中,我們可以透過特殊屬性 __proto__
設定原型
const proto = {
protoProp: 'a',
;
}const obj = {
__proto__: proto,
objProp: 'b',
;
}
// obj inherits .protoProp:
.equal(obj.protoProp, 'a');
assert.equal('protoProp' in obj, true); assert
由於原型物件本身可以有原型,因此我們會得到一個物件鏈,也就是所謂的原型鏈。繼承給我們一種錯覺,讓我們以為我們處理的是單一物件,但我們實際上處理的是物件鏈。
圖 9 顯示 obj
的原型鏈是什麼樣子。
未繼承的屬性稱為自有屬性。obj
有自有屬性 .objProp
。
有些運算會考慮所有屬性(自有和繼承的),例如取得屬性
> const obj = { one: 1 };
> typeof obj.one // own'number'
> typeof obj.toString // inherited'function'
其他運算只會考慮自有屬性,例如 Object.keys()
> Object.keys(obj)[ 'one' ]
請繼續閱讀另一個只考慮自有屬性的運算:設定屬性。
假設有一個物件 obj
有原型物件的鏈,設定 obj
的自身屬性只會改變 obj
是合理的。然而,透過 obj
設定繼承的屬性也只會改變 obj
。它會在 obj
中建立一個新的自身屬性來覆寫繼承的屬性。讓我們探討一下它如何與以下物件一起運作
const proto = {
protoProp: 'a',
;
}const obj = {
__proto__: proto,
objProp: 'b',
; }
在下一段程式碼片段中,我們設定繼承的屬性 obj.protoProp
(A 行)。透過建立一個自身屬性來「改變」它:當讀取 obj.protoProp
時,會先找到自身屬性,其值會覆寫繼承屬性的值。
// In the beginning, obj has one own property
.deepEqual(Object.keys(obj), ['objProp']);
assert
.protoProp = 'x'; // (A)
obj
// We created a new own property:
.deepEqual(Object.keys(obj), ['objProp', 'protoProp']);
assert
// The inherited property itself is unchanged:
.equal(proto.protoProp, 'a');
assert
// The own property overrides the inherited property:
.equal(obj.protoProp, 'x'); assert
obj
的原型鏈描繪在圖 10。
__proto__
的建議
不要使用 __proto__
作為偽屬性(Object
所有實例的設定器)
Object
實例的物件)。有關此功能的更多資訊,請參閱 §29.8.7 “Object.prototype.__proto__
(存取器)”。
在物件文字中使用 __proto__
來設定原型不同:它是物件文字的一項功能,沒有陷阱。
取得和設定原型的建議方式如下
取得物件的原型
Object.getPrototypeOf(obj: Object) : Object
設定物件原型的最佳時機是在我們建立它時。我們可以透過物件文字中的 __proto__
或透過
Object.create(proto: Object) : Object
如果必須,我們可以使用 Object.setPrototypeOf()
來改變現有物件的原型。但這可能會對效能造成負面影響。
以下是這些功能的使用方式
const proto1 = {};
const proto2a = {};
const proto2b = {};
const obj1 = {
__proto__: proto1,
a: 1,
b: 2,
;
}.equal(Object.getPrototypeOf(obj1), proto1);
assert
const obj2 = Object.create(
,
proto2a
{a: {
value: 1,
writable: true,
enumerable: true,
configurable: true,
,
}b: {
value: 2,
writable: true,
enumerable: true,
configurable: true,
,
}
};
).equal(Object.getPrototypeOf(obj2), proto2a);
assert
Object.setPrototypeOf(obj2, proto2b);
.equal(Object.getPrototypeOf(obj2), proto2b); assert
到目前為止,「proto
是 obj
的原型」總是表示「proto
是 obj
的直接原型」。但它也可以更寬鬆地使用,表示 proto
在 obj
的原型鏈中。這種較寬鬆的關係可以透過 .isPrototypeOf()
檢查
例如
const a = {};
const b = {__proto__: a};
const c = {__proto__: b};
.equal(a.isPrototypeOf(b), true);
assert.equal(a.isPrototypeOf(c), true);
assert
.equal(c.isPrototypeOf(a), false);
assert.equal(a.isPrototypeOf(a), false); assert
有關此方法的更多資訊,請參閱 §29.8.5 “Object.prototype.isPrototypeOf()
”。
Object.hasOwn()
:給定的屬性是自身(非繼承)的嗎?[ES2022]in
運算子(A 行)檢查物件是否具有給定的屬性。相反地,Object.hasOwn()
(B 行和 C 行)檢查屬性是否為自身。
const proto = {
protoProp: 'protoProp',
;
}const obj = {
__proto__: proto,
objProp: 'objProp',
}.equal('protoProp' in obj, true); // (A)
assert.equal(Object.hasOwn(obj, 'protoProp'), false); // (B)
assert.equal(Object.hasOwn(proto, 'protoProp'), true); // (C) assert
ES2022 之前的替代方案:
.hasOwnProperty()
在 ES2022 之前,我們可以使用另一個功能:§29.8.8 “Object.prototype.hasOwnProperty()
”。此功能有陷阱,但所引用的章節說明了如何解決這些陷阱。
考慮以下程式碼
const jane = {
firstName: 'Jane',
describe() {
return 'Person named '+this.firstName;
,
};
}const tarzan = {
firstName: 'Tarzan',
describe() {
return 'Person named '+this.firstName;
,
};
}
.equal(jane.describe(), 'Person named Jane');
assert.equal(tarzan.describe(), 'Person named Tarzan'); assert
我們有兩個非常相似的物件。兩個物件都有兩個屬性,其名稱為 .firstName
和 .describe
。此外,方法 .describe()
是相同的。我們如何避免複製該方法?
我們可以將它移到物件 PersonProto
,並使該物件成為 jane
和 tarzan
的原型
const PersonProto = {
describe() {
return 'Person named ' + this.firstName;
,
};
}const jane = {
__proto__: PersonProto,
firstName: 'Jane',
;
}const tarzan = {
__proto__: PersonProto,
firstName: 'Tarzan',
; }
原型的名稱反映出 jane
和 tarzan
都是人。
圖 11 說明了這三個物件是如何連接的:底部的物件現在包含特定於 jane
和 tarzan
的屬性。頂部的物件包含它們之間共有的屬性。
當我們呼叫方法 jane.describe()
時,this
指向該方法呼叫的接收者 jane
(在圖表的左下角)。這就是為什麼該方法仍然有效。tarzan.describe()
的運作方式類似。
.equal(jane.describe(), 'Person named Jane');
assert.equal(tarzan.describe(), 'Person named Tarzan'); assert
展望下一章關於類別的內容 – 這是類別在內部組織的方式
§29.3「類別的內部結構」 會更詳細地說明這一點。
原則上,物件是無序的。排序屬性的主要原因是讓列出項目、鍵或值的運算具有確定性。這有助於例如測試。
測驗
請參閱 測驗應用程式。