## 25.單個對象
> 原文: [http://exploringjs.com/impatient-js/ch_single-objects.html](http://exploringjs.com/impatient-js/ch_single-objects.html)
>
> 貢獻者:[阿純](https://github.com/rechun)
在本書中,JavaScript 的面向對象編程(OOP)風格分四步介紹。本章介紹步驟 1,[下一章](ch_proto-chains-classes.html)涵蓋步驟 2-4。步驟是(圖 [7](#fig:oop_steps1) ):
1. **單個對象:** _ 對象 _,JavaScript 的基本 OOP 構建塊如何獨立工作?
2. 原型鏈:每個對象都有一個零個或多個 _ 原型對象鏈 _。原型是 JavaScript 的核心繼承機制。
3. 類:JavaScript 的 _ 類 _ 是對象的工廠。類及其實例之間的關系基于原型繼承。
4. 子類化:_ 子類 _ 與其 _ 超類 _ 之間的關系也基于原型繼承。

Figure 7: This book introduces object-oriented programming in JavaScript in four steps.
### 25.1。 JavaScript 中對象的兩個角色
在 JavaScript 中,對象是一組稱為 _ 屬性 _ 的鍵值條目。
對象在 JavaScript 中扮演兩個角色:
* 記錄:作為記錄的對象具有固定數量的屬性,其密鑰在開發時是已知的。它們的值可以有不同的類型。本章首先介紹了這種使用對象的方法。
* 字典:Objects-as-dictionaries 具有可變數量的屬性,其密鑰在開發時是未知的。它們的所有值都具有相同的類型。通常最好將映射用作詞典而不是對象(本章稍后將對此進行介紹)。
### 25.2。對象作為記錄
#### 25.2.1。對象字面值:屬性
作為記錄的對象是通過所謂的 _ 對象字面值 _ 創建的。對象字面值是 JavaScript 的一個突出特點:它們允許您直接創建對象。不需要上課!這是一個例子:
```js
const jane = {
first: 'Jane',
last: 'Doe', // optional trailing comma
};
```
在這個例子中,我們通過一個對象字面值創建了一個對象,它以花括號`{}`開頭和結尾。在其中,我們定義了兩個 _ 屬性 _(鍵值條目):
* 第一個屬性具有鍵`first`和值`'Jane'`。
* 第二個屬性具有鍵`last`和值`'Doe'`。
如果以這種方式編寫它們,則屬性鍵必須遵循 JavaScript 變量名稱的規則,但允許使用保留字。
訪問屬性如下:
```js
assert.equal(jane.first, 'Jane'); // get property .first
jane.first = 'John'; // set property .first
assert.equal(jane.first, 'John');
```
#### 25.2.2。對象字面值:屬性值縮寫
每當通過變量名定義屬性的值并且該名稱與鍵相同時,您可以省略該鍵。
#### 25.2.3。術語:屬性鍵,屬性名稱,屬性符號
鑒于屬性鍵可以是字符串和符號,因此進行以下區分:
```js
const x = 4;
const y = 1;
assert.deepEqual(
{ x, y },
{ x: x, y: y }
);
```
* 如果屬性鍵是字符串,則它也稱為 _ 屬性名稱 _。
* 如果屬性鍵是符號,則它也稱為 _ 屬性符號 _。
此術語用于 JavaScript 標準庫(“自己”表示“未繼承”,將在下一章中介紹):
* `Object.keys(obj)`:返回`obj`的所有屬性鍵
* `Object.getOwnPropertyNames(obj)`
* `Object.getOwnPropertySymbols(obj)`
#### 25.2.4。獲得屬性
這就是你 _ 得到 _(讀)財產的方式:
```js
obj.propKey
```
如果`obj`沒有其鍵為`propKey`的屬性,則此表達式求值為`undefined`:
```js
const obj = {};
assert.equal(obj.propKey, undefined);
```
#### 25.2.5。設置屬性
這就是你 設置 (寫入)屬性的方式:
```js
obj.propKey = value;
```
如果`obj`已經有一個鍵為`propKey`的屬性,則此語句將更改該屬性。否則,它會創建一個新屬性:
```js
const obj = {};
assert.deepEqual(
Object.keys(obj), []);
obj.propKey = 123;
assert.deepEqual(
Object.keys(obj), ['propKey']);
```
#### 25.2.6。對象字面值:方法
以下代碼顯示如何通過對象字面值創建方法`.describe()`:
```js
const jane = {
first: 'Jane', // data property
says(text) { // method
return `${this.first} says “${text}”`; // (A)
}, // comma as separator (optional at end)
};
assert.equal(jane.says('hello'), 'Jane says “hello”');
```
在方法調用`jane.says('hello')`期間,`jane`被稱為方法調用的 _ 接收器 _,并被分配給特殊變量`this`。這使方法`.says()`能夠訪問 A 行中的兄弟屬性`.first`。
#### 25.2.7。對象字面值:訪問者
JavaScript 中有兩種訪問器:
* _getter_ 是 調用 (讀取)屬性調用的方法。
* _setter_ 是由 設置 (寫入)屬性調用的方法。
##### 25.2.7.1。getter
通過在方法定義前添加關鍵字`get`來創建 getter:
```js
const jane = {
first: 'Jane',
last: 'Doe',
get full() {
return `${this.first} ${this.last}`;
},
};
assert.equal(jane.full, 'Jane Doe');
jane.first = 'John';
assert.equal(jane.full, 'John Doe');
```
##### 25.2.7. setter
通過在方法定義前添加關鍵字`set`來創建 setter:
```js
const jane = {
first: 'Jane',
last: 'Doe',
set full(fullName) {
const parts = fullName.split(' ');
this.first = parts[0];
this.last = parts[1];
},
};
jane.full = 'Richard Roe';
assert.equal(jane.first, 'Richard');
assert.equal(jane.last, 'Roe');
```
 **練習:通過對象字面值創建對象**
`exercises/single-objects/color_point_object_test.js`
### 25.3。傳播到對象字面值(`...`)
我們已經看到在函數調用中使用擴展(`...`),它將迭代的內容轉換為參數。
在對象字面值內部,擴展屬性 將另一個對象的屬性添加到當前對象:
```js
> const obj = {foo: 1, bar: 2};
> {...obj, baz: 3}
{ foo: 1, bar: 2, baz: 3 }
```
如果屬性鍵發生沖突,則上次提到的屬性為“wins”:
```js
> 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 }
```
#### 25.3.1。傳播的用例:復制對象
您可以使用 spread 來創建對象的副本`original`:
```js
const copy = {...original};
```
警告 - 復制是 淺 :`copy`是一個新對象,帶有`original`的所有屬性(鍵值對)的副本。但是如果屬性值是對象,那么不會復制它們;它們在`original`和`copy`之間共享。以下代碼演示了這意味著什么。
```js
const original = { a: 1, b: {foo: true} };
const copy = {...original};
// The first level is a true copy:
assert.deepEqual(
copy, { a: 1, b: {foo: true} });
original.a = 2;
assert.deepEqual(
copy, { a: 1, b: {foo: true} }); // no change
// Deeper levels are not copied:
original.b.foo = false;
// The value of property `b` is shared
// between original and copy.
assert.deepEqual(
copy, { a: 1, b: {foo: false} });
```
 ** JavaScript 不做深拷貝**
_ 對象的深拷貝 _(所有級別都被復制)是眾所周知的難以做到的。因此,JavaScript 沒有內置的操作(暫時)。如果您需要這樣的操作,則必須自己實現。
#### 25.3.2。傳播的用例:缺失屬性的默認值
如果代碼的其中一個輸入是包含數據的對象,則可以在為其指定默認值時使屬性成為可選屬性。這樣做的一種技術是通過其屬性包含默認值的對象。在以下示例中,該對象是`DEFAULTS`:
```js
const DEFAULTS = {foo: 'a', bar: 'b'};
const providedData = {foo: 1};
const allData = {...DEFAULTS, ...providedData};
assert.deepEqual(allData, {foo: 1, bar: 'b'});
```
結果,對象`allData`是通過創建`DEFAULTS`的副本并用`providedData`覆蓋其屬性來創建的。
但是您不需要對象來指定默認值,您也可以單獨在對象字面值中指定它們:
```js
const providedData = {foo: 1};
const allData = {foo: 'a', bar: 'b', ...providedData};
assert.deepEqual(allData, {foo: 1, bar: 'b'});
```
#### 25.3.3。用例傳播:非破壞性變化的屬性
到目前為止,我們遇到了一種更改對象屬性的方法:我們 _ 設置 _ 并改變對象。也就是說,這種改變屬性的方式是 _ 破壞性 _
通過傳播,您可以非破壞性地更改屬性:您可以復制屬性具有不同值的對象。
例如,此代碼非破壞性地更新屬性`.foo`:
```js
const obj = {foo: 'a', bar: 'b'};
const updatedObj = {...obj, foo: 1};
assert.deepEqual(updatedObj, {foo: 1, bar: 'b'});
```
 **練習:通過傳播(固定密鑰)非破壞性更新屬性**
`exercises/single-objects/update_name_test.js`
### 25.4。方法
#### 25.4.1。方法是值為函數的屬性
讓我們重溫一下用于介紹方法的示例:
```js
const jane = {
first: 'Jane',
says(text) {
return `${this.first} says “${text}”`;
},
};
```
有些令人驚訝的是,方法是函數:
```js
assert.equal(typeof jane.says, 'function');
```
這是為什么?請記住,在[關于可調用實體](ch_callables.html#roles-of-ordinary-functions)的章節中,我們了解到普通函數扮演了幾個角色。 _ 方法 _ 是其中一個角色。因此,在引擎蓋下,`jane`大致如下所示。
```js
const jane = {
first: 'Jane',
says: function (text) {
return `${this.first} says “${text}”`;
},
};
```
#### 25.4.2。 `.call()`:顯式參數`this`
請記住,每個函數`someFunc`也是一個對象,因此有方法。一種這樣的方法是`.call()` - 它允許您在明確指定`this`時調用函數:
```js
someFunc.call(thisValue, arg1, arg2, arg3);
```
##### 25.4.2.1。方法和`.call()`
如果進行方法調用,`this`始終是隱式參數:
```js
const obj = {
method(x) {
assert.equal(this, obj); // implicit parameter
assert.equal(x, 'a');
},
};
obj.method('a');
// Equivalent:
obj.method.call(obj, 'a');
```
順便說一句,這意味著實際上有兩個不同的點運算符:
1. 一個用于訪問屬性:`obj.prop`
2. 一個用于進行方法調用:`obj.prop()`
它們的不同之處在于(2)不僅僅是(1),后面是函數調用運算符`()`。相反,(2)另外指定`this`的值(如前面的例子所示)。
##### 25.4.2.2。功能和`.call()`
但是,如果函數調用普通函數,`this`也是一個隱式參數:
```js
function func(x) {
assert.equal(this, undefined); // implicit parameter
assert.equal(x, 'a');
}
func('a');
// Equivalent:
func.call(undefined, 'a');
```
也就是說,在函數調用期間,普通函數具有`this`,但它被設置為`undefined`,這表示它在這里沒有真正的用途。
接下來,我們將研究使用`this`的缺陷。在我們能夠做到這一點之前,我們還需要一個工具:函數的方法`.bind()`。
#### 25.4.3。 `.bind()`:預填充`this`和功能參數
`.bind()`是函數對象的另一種方法。調用此方法如下。
```js
const boundFunc = someFunc.bind(thisValue, arg1, arg2, arg3);
```
`.bind()`返回一個新函數`boundFunc()`。調用該函數調用`someFunc()`并將`this`設置為`thisValue`并且這些參數:`arg1`,`arg2`,`arg3`,然后是`boundFunc()`的參數。
也就是說,以下兩個函數調用是等效的:
```js
boundFunc('a', 'b')
someFunc.call(thisValue, arg1, arg2, arg3, 'a', 'b')
```
另一種預填`this`和參數的方法是通過箭頭功能:
```js
const boundFunc2 = (...args) =>
someFunc.call(thisValue, arg1, arg2, arg3, ...args);
```
因此,`.bind()`可以實現為如下的實際功能:
```js
function bind(func, thisValue, ...boundArgs) {
return (...args) =>
func.call(thisValue, ...boundArgs, ...args);
}
```
##### 25.4.3.1。示例:綁定實際函數
將`.bind()`用于實際功能有點不直觀,因為你必須為`this`提供一個值。該值通常是`undefined`,反映了函數調用期間發生的情況。
在下面的示例中,我們通過將`add()`的第一個參數綁定到`8`來創建`add8()`,這是一個具有一個參數的函數。
```js
function add(x, y) {
return x + y;
}
const add8 = add.bind(undefined, 8);
assert.equal(add8(1), 9);
```
##### 25.4.3.2。示例:綁定方法
在下面的代碼中,我們將方法`.says()`轉換為獨立函數`func()`:
```js
const jane = {
first: 'Jane',
says(text) {
return `${this.first} says “${text}”`; // (A)
},
};
const func = jane.says.bind(jane, 'hello');
assert.equal(func(), 'Jane says “hello”');
```
通過`.bind()`將`this`設置為`jane`至關重要。否則,`func()`將無法正常工作,因為在行 A 中使用了`this`。
#### 25.4.4。 `this`陷阱:提取方法
我們現在對函數和方法有了很多了解,并準備好了解涉及方法和`this`的最大缺陷:如果你不小心,函數調用從對象中提取的方法可能會失敗。
在下面的例子中,當我們提取方法`jane.says()`時,我們失敗,將它存儲在變量`func`和函數調用`func()`中。
```js
const jane = {
first: 'Jane',
says(text) {
return `${this.first} says “${text}”`;
},
};
const func = jane.says; // extract the method
assert.throws(
() => func('hello'), // (A)
{
name: 'TypeError',
message: "Cannot read property 'first' of undefined",
});
```
A 行中的函數調用相當于:
```js
assert.throws(
() => jane.says.call(undefined, 'hello'), // `this` is undefined!
{
name: 'TypeError',
message: "Cannot read property 'first' of undefined",
});
```
那么我們如何解決這個問題呢?我們需要使用`.bind()`來提取方法`.says()`:
```js
const func2 = jane.says.bind(jane);
assert.equal(func2('hello'), 'Jane says “hello”');
```
當我們調用`func()`時,`.bind()`確保`this`始終為`jane`。
您還可以使用箭頭函數來提取方法:
```js
const func3 = text => jane.says(text);
assert.equal(func3('hello'), 'Jane says “hello”');
```
##### 25.4.4.1。示例:提取方法
以下是您在實際 Web 開發中可能看到的代碼的簡化版本:
```js
class ClickHandler {
constructor(elem) {
elem.addEventListener('click', this.handleClick); // (A)
}
handleClick(event) {
alert('Clicked!');
}
}
```
在 A 行中,我們沒有正確提取方法`.handleClick()`。相反,我們應該這樣做:
```js
elem.addEventListener('click', this.handleClick.bind(this));
```
 **練習:提取方法**
`exercises/single-objects/method_extraction_exrc.js`
#### 25.4.5。 `this`陷阱:意外遮蔽`this`
如果使用普通功能,意外遮蔽`this`只是一個問題。
請考慮以下問題:當您在普通函數內部時,您無法訪問周圍范圍的`this`,因為普通函數有自己的`this`。換句話說:內部作用域中的變量將變量隱藏在外部作用域中。這被稱為 _ 陰影 _。以下代碼是一個示例:
```js
const obj = {
name: 'Jane',
sayHiTo(friends) {
return friends.map(
function (friend) { // (A)
return `${this.name} says hi to ${friend}`; // (B)
});
}
};
assert.throws(
() => obj.sayHiTo(['Tarzan', 'Cheeta']),
{
name: 'TypeError',
message: "Cannot read property 'name' of undefined",
});
```
為什么錯誤? B 行中的`this`不是`.sayHiTo()`的`this`,它是從 B 行開始的普通函數的`this`。
有幾種方法可以解決這個問題。最簡單的方法是使用箭頭函數 - 它沒有自己的`this`,因此陰影不是問題。
```js
const obj = {
name: 'Jane',
sayHiTo(friends) {
return friends.map(
(friend) => {
return `${this.name} says hi to ${friend}`;
});
}
};
assert.deepEqual(
obj.sayHiTo(['Tarzan', 'Cheeta']),
['Jane says hi to Tarzan', 'Jane says hi to Cheeta']);
```
#### 25.4.6。避免`this`的陷阱
我們已經看到了兩個與`this`相關的重大陷阱:
1. [提取方法](ch_single-objects.html#this-pitfall-extracting-methods)
2. [意外遮蔽`this`](ch_single-objects.html#this-pitfall-shadowing)
一個簡單的規則有助于避免第二個陷阱:
> “避免關鍵字`function`”:絕不使用普通函數,只使用箭頭函數(用于實際函數)和方法定義。
讓我們打破這個規則:
* 如果所有實際函數都是箭頭函數,則第二個陷阱永遠不會發生。
* 使用方法定義意味著您只能在方法中看到`this`,這使得此功能不那么混亂。
但是,即使我不使用(普通)函數 _ 表達式 _,我還是在語法上喜歡函數 _ 聲明 _。如果您沒有參考其中的`this`,您可以安全地使用它們。檢查工具 ESLint 有[規則](https://eslint.org/docs/rules/no-invalid-this),有助于此。
唉,第一個陷阱沒有簡單的方法:每當你提取一個方法時,你必須小心并正確地做到這一點。例如,通過綁定`this`。
#### 25.4.7。 `this`在各種情況下的值
`this`在各種情況下的值是多少?
在可調用實體中,`this`的值取決于調用可調用實體的方式以及它是什么類型的可調用實體:
* 功能調用:
* 普通功能:`this === undefined`
* 箭頭功能:`this`與周圍范圍相同(詞匯`this`)
* 方法調用:`this`是呼叫接收方
* `new`:`this`是指新創建的實例
您還可以在所有常見的頂級范圍中訪問`this`:
* `<script>`元素:`this === window`
* ES 模塊:`this === undefined`
* CommonJS 模塊:`this === module.exports`
但是,我喜歡假裝您無法訪問頂級作用域中的`this`,因為頂級`this`令人困惑且沒有用處。
### 25.5。對象作為詞典
對象最適合作為記錄。但在 ES6 之前,JavaScript 沒有字典的數據結構(ES6 帶來了映射)。因此,必須將對象用作字典。因此,鍵必須是字符串,但值可以是任意類型。
我們首先看一下與字典相關的對象的特征,但偶爾也可用于對象作為記錄。本節最后提供了實際使用對象作為詞典的提示(劇透:如果可以,請避免使用映射)。
#### 25.5.1。任意固定字符串作為屬性鍵
從對象作為記錄到對象作為字典時,一個重要的變化是我們必須能夠使用任意字符串作為屬性鍵。本小節解釋了如何實現固定字符串鍵。下一小節將介紹如何動態計算任意鍵。
到目前為止,我們只看到合法的 JavaScript 標識符作為屬性鍵(符號除外):
```js
const obj = {
mustBeAnIdentifier: 123,
};
// Get property
assert.equal(obj.mustBeAnIdentifier, 123);
// Set property
obj.mustBeAnIdentifier = 'abc';
assert.equal(obj.mustBeAnIdentifier, 'abc');
```
兩種技術允許我們使用任意字符串作為屬性鍵。
首先 - 當通過對象字面值創建屬性鍵時,我們可以引用屬性鍵(帶單引號或雙引號):
```js
const obj = {
'Can be any string!': 123,
};
```
第二 - 獲取或設置屬性時,我們可以使用帶有字符串的方括號:
```js
// Get property
assert.equal(obj['Can be any string!'], 123);
// Set property
obj['Can be any string!'] = 'abc';
assert.equal(obj['Can be any string!'], 'abc');
```
您還可以引用方法的鍵:
```js
const obj = {
'A nice method'() {
return 'Yes!';
},
};
assert.equal(obj['A nice method'](), 'Yes!');
```
#### 25.5.2。計算屬性鍵
到目前為止,我們受到了對象字面值內部屬性鍵的限制:它們總是固定的,它們總是字符串。如果我們將表達式放在方括號中,我們可以動態計算任意鍵:
```js
const obj = {
['Hello world!']: true,
['f'+'o'+'o']: 123,
[Symbol.toStringTag]: 'Goodbye', // (A)
};
assert.equal(obj['Hello world!'], true);
assert.equal(obj.foo, 123);
assert.equal(obj[Symbol.toStringTag], 'Goodbye');
```
計算鍵的主要用例是將符號作為屬性鍵(行 A)。
請注意,用于獲取和設置屬性的方括號運算符適用于任意表達式:
```js
assert.equal(obj['f'+'o'+'o'], 123);
assert.equal(obj['==> foo'.slice(-3)], 123);
```
方法也可以有計算屬性鍵:
```js
const methodKey = Symbol();
const obj = {
[methodKey]() {
return 'Yes!';
},
};
assert.equal(obj[methodKey](), 'Yes!');
```
我們現在切換回固定屬性鍵,但如果需要計算屬性鍵,則可以始終使用方括號。
 **練習:通過傳播非破壞性更新屬性(計算密鑰)**
`exercises/single-objects/update_property_test.js`
#### 25.5.3。 `in`運算符:是否存在具有給定鍵的屬性?
`in`運算符檢查對象是否具有給定鍵的屬性:
```js
const obj = {
foo: 'abc',
bar: false,
};
assert.equal('foo' in obj, true);
assert.equal('unknownKey' in obj, false);
```
##### 25.5.3.1。通過真實性檢查財產是否存在
您還可以使用真實性檢查來確定屬性是否存在:
```js
assert.equal(
obj.unknownKey ? 'exists' : 'does not exist',
'does not exist');
assert.equal(
obj.foo ? 'exists' : 'does not exist',
'exists');
```
之前的檢查有效,因為讀取不存在的屬性會返回`undefined`,這是假的。因為`obj.foo`是真實的。
但是,有一個重要的警告:如果屬性存在,則真實性檢查失敗,但具有假值(`undefined`,`null`,`false`,`0`,`""`等):
```js
assert.equal(
obj.bar ? 'exists' : 'does not exist',
'does not exist'); // should be: 'exists'
```
#### 25.5.4。刪除屬性
您可以通過`delete`運算符刪除屬性:
```js
const obj = {
foo: 123,
};
assert.deepEqual(Object.keys(obj), ['foo']);
delete obj.foo;
assert.deepEqual(Object.keys(obj), []);
```
#### 25.5.5。字典陷阱
如果使用普通對象(通過對象字面值創建)作為字典,則必須注意兩個陷阱。
第一個缺陷是`in`運算符還找到了繼承的屬性:
```js
const dict = {};
assert.equal('toString' in dict, true);
```
我們希望`dict`被視為空,但`in`運算符會檢測它從原型`Object.prototype`繼承的屬性。
第二個缺陷是你不能使用屬性鍵`__proto__`,因為它具有特殊的權力(它設置了對象的原型):
```js
const dict = {};
dict['__proto__'] = 123;
// No property was added to dict:
assert.deepEqual(Object.keys(dict), []);
```
那么我們如何解決這些陷阱呢?
* 只要你可以,使用映射。它們是詞典的最佳解決方案。
* 如果你不能:將庫用于可以安全地完成所有操作的對象字典。
* 如果你不能:使用沒有原型的對象。這消除了現代 JavaScript 中的兩個陷阱。
```js
const dict = Object.create(null); // no prototype
assert.equal('toString' in dict, false);
dict['__proto__'] = 123;
assert.deepEqual(Object.keys(dict), ['__proto__']);
```
 **練習:使用對象作為字典**
`exercises/single-objects/simple_dict_test.js`
#### 25.5.6。列出屬性鍵
Table 19: Standard library methods for listing _own_ (non-inherited) property keys. All of them return Arrays with strings and/or symbols.
| | 枚舉 | 沒有。 | 串 | 符號 |
| --- | --- | --- | --- | --- |
| `Object.keys()` | `?` | | `?` | |
| `Object.getOwnPropertyNames()` | `?` | `?` | `?` | |
| `Object.getOwnPropertySymbols()` | `?` | `?` | | `?` |
| `Reflect.ownKeys()` | `?` | `?` | `?` | `?` |
tbl 中的每個方法。 [19](#tbl:listing-property-keys) 返回一個帶有參數自身屬性鍵的數組。在方法的名稱中,您可以看到我們之前討論過的屬性鍵(字符串和符號),屬性名稱(僅字符串)和屬性符號(僅符號)之間的區別。
可枚舉性是屬性的 _ 屬性 _。默認情況下,屬性是可枚舉的,但有一些方法可以改變它(在下一個示例中顯示,[稍后將詳細描述](ch_single-objects.html#property-attributes))。
例如:
```js
const enumerableSymbolKey = Symbol('enumerableSymbolKey');
const nonEnumSymbolKey = Symbol('nonEnumSymbolKey');
// We create the enumerable properties via an object literal
const obj = {
enumerableStringKey: 1,
[enumerableSymbolKey]: 2,
}
// For the non-enumerable properties,
// we need a more powerful tool:
Object.defineProperties(obj, {
nonEnumStringKey: {
value: 3,
enumerable: false,
},
[nonEnumSymbolKey]: {
value: 4,
enumerable: false,
},
});
assert.deepEqual(
Object.keys(obj),
[ 'enumerableStringKey' ]);
assert.deepEqual(
Object.getOwnPropertyNames(obj),
[ 'enumerableStringKey', 'nonEnumStringKey' ]);
assert.deepEqual(
Object.getOwnPropertySymbols(obj),
[ enumerableSymbolKey, nonEnumSymbolKey ]);
assert.deepEqual(
Reflect.ownKeys(obj),
[ 'enumerableStringKey',
'nonEnumStringKey',
enumerableSymbolKey,
nonEnumSymbolKey ]);
```
#### 25.5.7。通過`Object.values()`列出屬性值
`Object.values()`列出對象的所有可枚舉屬性的值:
```js
const obj = {foo: 1, bar: 2};
assert.deepEqual(
Object.values(obj),
[1, 2]);
```
#### 25.5.8。通過`Object.entries()`列出屬性條目
`Object.entries()`列出了可枚舉屬性的鍵值對。每對編碼為一個雙元素數組:
```js
const obj = {foo: 1, bar: 2};
assert.deepEqual(
Object.entries(obj),
[
['foo', 1],
['bar', 2],
]);
```
 **練習:`Object.entries()`**
`exercises/single-objects/find_key_test.js`
#### 25.5.9。確定性地列出屬性
對象的自有(非繼承)屬性始終按以下順序列出:
1. 具有整數索引的屬性(例如,數組索引)
* 按升序數字順序
1. 帶字符串鍵的剩余屬性
* 按照添加順序
1. 帶符號鍵的屬性
* 按照添加順序
以下示例演示如何根據以下規則對屬性鍵進行排序:
```js
> Object.keys({b:0,a:0, 2:0,1:0})
[ '1', '2', 'b', 'a' ]
```
([您可以在規格中查找詳細信息。](https://tc39.github.io/ecma262/#sec-ordinaryownpropertykeys))
#### 25.5.10。通過`Object.fromEntries()`組裝對象
給定[key,value]對可迭代,`Object.fromEntries()`創建一個對象:
```js
assert.deepEqual(
Object.fromEntries([['foo',1], ['bar',2]]),
{
foo: 1,
bar: 2,
}
);
```
它與 [`Object.entries()`](ch_single-objects.html#Object.entries) 相反。
接下來,我們將使用`Object.entries()`和`Object.fromEntries()`從庫 [Underscore](https://underscorejs.org) 中實現多個工具功能。
##### 25.5.10.1。示例:`pick(object, ...keys)`
[`pick()`](https://underscorejs.org/#pick) 從`object`中刪除其鍵不在`keys`中的所有屬性。刪除 _ 非破壞性 _:`pick()`創建修改后的副本并且不會更改原始文件。例如:
```js
const address = {
street: 'Evergreen Terrace',
number: '742',
city: 'Springfield',
state: 'NT',
zip: '49007',
};
assert.deepEqual(
pick(address, 'street', 'number'),
{
street: 'Evergreen Terrace',
number: '742',
}
);
```
我們可以按如下方式實現`pick()`:
```js
function pick(object, ...keys) {
const filteredEntries = Object.entries(object)
.filter(([key, _value]) => keys.includes(key));
return Object.fromEntries(filteredEntries);
}
```
##### 25.5.10.2。示例:`invert(object)`
[`invert()`](https://underscorejs.org/#invert) 非破壞性地交換鍵和對象的值:
```js
assert.deepEqual(
invert({a: 1, b: 2, c: 3}),
{1: 'a', 2: 'b', 3: 'c'}
);
```
我們可以像這樣實現它:
```js
function invert(object) {
const mappedEntries = Object.entries(object)
.map(([key, value]) => [value, key]);
return Object.fromEntries(mappedEntries);
}
```
##### 25.5.10.3。 `Object.fromEntries()`的簡單實現
`Object.fromEntries()`可以實現如下(我省略了幾個檢查):
```js
function fromEntries(iterable) {
const result = {};
for (const [key, value] of iterable) {
let coercedKey;
if (typeof key === 'string' || typeof key === 'symbol') {
coercedKey = key;
} else {
coercedKey = String(key);
}
Object.defineProperty(result, coercedKey, {
value,
writable: true,
enumerable: true,
configurable: true,
});
}
return result;
}
```
筆記:
* `Object.defineProperty()`在本章后面中解釋[。](ch_single-objects.html#property-attributes)
* 官方 polyfill 可通過 [npm 包`object.fromentries`](https://github.com/es-shims/Object.fromEntries) 獲得。
 **練習:`Object.entries()`和`Object.fromEntries()`**
`exercises/single-objects/omit_properties_test.js`
### 25.6。標準方法
`Object.prototype`定義了幾個可以覆蓋的標準方法。兩個重要的是:
* `.toString()`
* `.valueOf()`
粗略地說,`.toString()`配置對象如何轉換為字符串:
```js
> String({toString() { return 'Hello!' }})
'Hello!'
> String({})
'[object Object]'
```
并且`.valueOf()`配置對象如何轉換為數字:
```js
> Number({valueOf() { return 123 }})
123
> Number({})
NaN
```
### 25.7。高級主題
以下小節簡要概述了超出本書范圍的主題。
#### 25.7.1。 `Object.assign()`
`Object.assign()`是一種工具方法:
```js
Object.assign(target, source_1, source_2, ···)
```
這個表達式(破壞性地)將`source_1`合并到`target`,然后`source_2`等。最后,它返回`target`。例如:
```js
const target = { foo: 1 };
const result = Object.assign(
target,
{bar: 2},
{baz: 3, bar: 4});
assert.deepEqual(
result, { foo: 1, bar: 4, baz: 3 });
// target was changed!
assert.deepEqual(target, result);
```
`Object.assign()`的用例類似于傳播屬性的用例。在某種程度上,它破壞性地傳播。
有關`Object.assign()`的更多信息,請參閱[“探索 ES6”](http://exploringjs.com/es6/ch_oop-besides-classes.html#Object_assign)。
#### 25.7.2。凍結對象
`Object.freeze(obj)`使`obj`不可變:您無法更改或添加屬性或更改`obj`的原型。
例如:
```js
const frozen = Object.freeze({ x: 2, y: 5 });
assert.throws(
() => { frozen.x = 7 },
{
name: 'TypeError',
message: /^Cannot assign to read only property 'x'/,
});
```
有一點需要注意:`Object.freeze(obj)`淺薄地凍結。也就是說,只凍結`obj`的屬性,而不凍結屬性中存儲的對象。
有關`Object.freeze()`的更多信息,請參閱[“Speaking JavaScript”](http://speakingjs.com/es5/ch17.html#freezing_objects)。
#### 25.7.3。屬性屬性和屬性描述符
就像對象由屬性組成一樣,屬性由 _ 屬性 _ 組成。也就是說,您可以配置的不僅僅是屬性的值 - 這只是幾個屬性中的一個。其他屬性包括:
* `writable`:是否可以更改屬性的值?
* `enumerable`:`Object.keys()`是否列出了該屬性?
當您使用其中一個操作來訪問屬性屬性時,通過 _ 屬性描述符 _ 指定屬性:每個屬性代表一個屬性的對象。例如,這是您閱讀屬性`obj.foo`的屬性的方法:
```js
const obj = { foo: 123 };
assert.deepEqual(
Object.getOwnPropertyDescriptor(obj, 'foo'),
{
value: 123,
writable: true,
enumerable: true,
configurable: true,
});
```
這就是你設置屬性`obj.bar`的屬性的方法:
```js
const obj = {
foo: 1,
bar: 2,
};
assert.deepEqual(Object.keys(obj), ['foo', 'bar']);
// Hide property `bar` from Object.keys()
Object.defineProperty(obj, 'bar', {
enumerable: false,
});
assert.deepEqual(Object.keys(obj), ['foo']);
```
有關屬性屬性和屬性描述符的更多信息,請參閱[“Speaking JavaScript”](http://speakingjs.com/es5/ch17.html#property_attributes)。
 **測驗**
參見[測驗應用程序](ch_quizzes-exercises.html#quizzes)。
- I.背景
- 1.關于本書(ES2019 版)
- 2.常見問題:本書
- 3. JavaScript 的歷史和演變
- 4.常見問題:JavaScript
- II.第一步
- 5.概覽
- 6.語法
- 7.在控制臺上打印信息(console.*)
- 8.斷言 API
- 9.測驗和練習入門
- III.變量和值
- 10.變量和賦值
- 11.值
- 12.運算符
- IV.原始值
- 13.非值undefined和null
- 14.布爾值
- 15.數字
- 16. Math
- 17. Unicode - 簡要介紹(高級)
- 18.字符串
- 19.使用模板字面值和標記模板
- 20.符號
- V.控制流和數據流
- 21.控制流語句
- 22.異常處理
- 23.可調用值
- VI.模塊化
- 24.模塊
- 25.單個對象
- 26.原型鏈和類
- 七.集合
- 27.同步迭代
- 28.數組(Array)
- 29.類型化數組:處理二進制數據(高級)
- 30.映射(Map)
- 31. WeakMaps(WeakMap)
- 32.集(Set)
- 33. WeakSets(WeakSet)
- 34.解構
- 35.同步生成器(高級)
- 八.異步
- 36. JavaScript 中的異步編程
- 37.異步編程的 Promise
- 38.異步函數
- IX.更多標準庫
- 39.正則表達式(RegExp)
- 40.日期(Date)
- 41.創建和解析 JSON(JSON)
- 42.其余章節在哪里?