# 對象的擴展
對象(object)是 JavaScript 最重要的數據結構。ES6 對它進行了重大升級,本章介紹數據結構本身的改變,下一章介紹`Object`對象的新增方法。
## 屬性的簡潔表示法
ES6 允許在大括號里面,直接寫入變量和函數,作為對象的屬性和方法。這樣的書寫更加簡潔。
```javascript
const foo = 'bar';
const baz = {foo};
baz // {foo: "bar"}
// 等同于
const baz = {foo: foo};
```
上面代碼中,變量`foo`直接寫在大括號里面。這時,屬性名就是變量名, 屬性值就是變量值。下面是另一個例子。
```javascript
function f(x, y) {
return {x, y};
}
// 等同于
function f(x, y) {
return {x: x, y: y};
}
f(1, 2) // Object {x: 1, y: 2}
```
除了屬性簡寫,方法也可以簡寫。
```javascript
const o = {
method() {
return "Hello!";
}
};
// 等同于
const o = {
method: function() {
return "Hello!";
}
};
```
下面是一個實際的例子。
```javascript
let birth = '2000/01/01';
const Person = {
name: '張三',
//等同于birth: birth
birth,
// 等同于hello: function ()...
hello() { console.log('我的名字是', this.name); }
};
```
這種寫法用于函數的返回值,將會非常方便。
```javascript
function getPoint() {
const x = 1;
const y = 10;
return {x, y};
}
getPoint()
// {x:1, y:10}
```
CommonJS 模塊輸出一組變量,就非常合適使用簡潔寫法。
```javascript
let ms = {};
function getItem (key) {
return key in ms ? ms[key] : null;
}
function setItem (key, value) {
ms[key] = value;
}
function clear () {
ms = {};
}
module.exports = { getItem, setItem, clear };
// 等同于
module.exports = {
getItem: getItem,
setItem: setItem,
clear: clear
};
```
屬性的賦值器(setter)和取值器(getter),事實上也是采用這種寫法。
```javascript
const cart = {
_wheels: 4,
get wheels () {
return this._wheels;
},
set wheels (value) {
if (value < this._wheels) {
throw new Error('數值太小了!');
}
this._wheels = value;
}
}
```
簡潔寫法在打印對象時也很有用。
```javascript
let user = {
name: 'test'
};
let foo = {
bar: 'baz'
};
console.log(user, foo)
// {name: "test"} {bar: "baz"}
console.log({user, foo})
// {user: {name: "test"}, foo: {bar: "baz"}}
```
上面代碼中,`console.log`直接輸出`user`和`foo`兩個對象時,就是兩組鍵值對,可能會混淆。把它們放在大括號里面輸出,就變成了對象的簡潔表示法,每組鍵值對前面會打印對象名,這樣就比較清晰了。
注意,簡寫的對象方法不能用作構造函數,會報錯。
```javascript
const obj = {
f() {
this.foo = 'bar';
}
};
new obj.f() // 報錯
```
上面代碼中,`f`是一個簡寫的對象方法,所以`obj.f`不能當作構造函數使用。
## 屬性名表達式
JavaScript 定義對象的屬性,有兩種方法。
```javascript
// 方法一
obj.foo = true;
// 方法二
obj['a' + 'bc'] = 123;
```
上面代碼的方法一是直接用標識符作為屬性名,方法二是用表達式作為屬性名,這時要將表達式放在方括號之內。
但是,如果使用字面量方式定義對象(使用大括號),在 ES5 中只能使用方法一(標識符)定義屬性。
```javascript
var obj = {
foo: true,
abc: 123
};
```
ES6 允許字面量定義對象時,用方法二(表達式)作為對象的屬性名,即把表達式放在方括號內。
```javascript
let propKey = 'foo';
let obj = {
[propKey]: true,
['a' + 'bc']: 123
};
```
下面是另一個例子。
```javascript
let lastWord = 'last word';
const a = {
'first word': 'hello',
[lastWord]: 'world'
};
a['first word'] // "hello"
a[lastWord] // "world"
a['last word'] // "world"
```
表達式還可以用于定義方法名。
```javascript
let obj = {
['h' + 'ello']() {
return 'hi';
}
};
obj.hello() // hi
```
注意,屬性名表達式與簡潔表示法,不能同時使用,會報錯。
```javascript
// 報錯
const foo = 'bar';
const bar = 'abc';
const baz = { [foo] };
// 正確
const foo = 'bar';
const baz = { [foo]: 'abc'};
```
注意,屬性名表達式如果是一個對象,默認情況下會自動將對象轉為字符串`[object Object]`,這一點要特別小心。
```javascript
const keyA = {a: 1};
const keyB = {b: 2};
const myObject = {
[keyA]: 'valueA',
[keyB]: 'valueB'
};
myObject // Object {[object Object]: "valueB"}
```
上面代碼中,`[keyA]`和`[keyB]`得到的都是`[object Object]`,所以`[keyB]`會把`[keyA]`覆蓋掉,而`myObject`最后只有一個`[object Object]`屬性。
## 方法的 name 屬性
函數的`name`屬性,返回函數名。對象方法也是函數,因此也有`name`屬性。
```javascript
const person = {
sayName() {
? ?console.log('hello!');
},
};
person.sayName.name // "sayName"
```
上面代碼中,方法的`name`屬性返回函數名(即方法名)。
如果對象的方法使用了取值函數(`getter`)和存值函數(`setter`),則`name`屬性不是在該方法上面,而是該方法的屬性的描述對象的`get`和`set`屬性上面,返回值是方法名前加上`get`和`set`。
```javascript
const obj = {
get foo() {},
set foo(x) {}
};
obj.foo.name
// TypeError: Cannot read property 'name' of undefined
const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');
descriptor.get.name // "get foo"
descriptor.set.name // "set foo"
```
有兩種特殊情況:`bind`方法創造的函數,`name`屬性返回`bound`加上原函數的名字;`Function`構造函數創造的函數,`name`屬性返回`anonymous`。
```javascript
(new Function()).name // "anonymous"
var doSomething = function() {
// ...
};
doSomething.bind().name // "bound doSomething"
```
如果對象的方法是一個 Symbol 值,那么`name`屬性返回的是這個 Symbol 值的描述。
```javascript
const key1 = Symbol('description');
const key2 = Symbol();
let obj = {
[key1]() {},
[key2]() {},
};
obj[key1].name // "[description]"
obj[key2].name // ""
```
上面代碼中,`key1`對應的 Symbol 值有描述,`key2`沒有。
## 屬性的可枚舉性和遍歷
### 可枚舉性
對象的每個屬性都有一個描述對象(Descriptor),用來控制該屬性的行為。`Object.getOwnPropertyDescriptor`方法可以獲取該屬性的描述對象。
```javascript
let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
// {
// value: 123,
// writable: true,
// enumerable: true,
// configurable: true
// }
```
描述對象的`enumerable`屬性,稱為“可枚舉性”,如果該屬性為`false`,就表示某些操作會忽略當前屬性。
目前,有四個操作會忽略`enumerable`為`false`的屬性。
- `for...in`循環:只遍歷對象自身的和繼承的可枚舉的屬性。
- `Object.keys()`:返回對象自身的所有可枚舉的屬性的鍵名。
- `JSON.stringify()`:只串行化對象自身的可枚舉的屬性。
- `Object.assign()`: 忽略`enumerable`為`false`的屬性,只拷貝對象自身的可枚舉的屬性。
這四個操作之中,前三個是 ES5 就有的,最后一個`Object.assign()`是 ES6 新增的。其中,只有`for...in`會返回繼承的屬性,其他三個方法都會忽略繼承的屬性,只處理對象自身的屬性。實際上,引入“可枚舉”(`enumerable`)這個概念的最初目的,就是讓某些屬性可以規避掉`for...in`操作,不然所有內部屬性和方法都會被遍歷到。比如,對象原型的`toString`方法,以及數組的`length`屬性,就通過“可枚舉性”,從而避免被`for...in`遍歷到。
```javascript
Object.getOwnPropertyDescriptor(Object.prototype, 'toString').enumerable
// false
Object.getOwnPropertyDescriptor([], 'length').enumerable
// false
```
上面代碼中,`toString`和`length`屬性的`enumerable`都是`false`,因此`for...in`不會遍歷到這兩個繼承自原型的屬性。
另外,ES6 規定,所有 Class 的原型的方法都是不可枚舉的。
```javascript
Object.getOwnPropertyDescriptor(class {foo() {}}.prototype, 'foo').enumerable
// false
```
總的來說,操作中引入繼承的屬性會讓問題復雜化,大多數時候,我們只關心對象自身的屬性。所以,盡量不要用`for...in`循環,而用`Object.keys()`代替。
### 屬性的遍歷
ES6 一共有 5 種方法可以遍歷對象的屬性。
**(1)for...in**
`for...in`循環遍歷對象自身的和繼承的可枚舉屬性(不含 Symbol 屬性)。
**(2)Object.keys(obj)**
`Object.keys`返回一個數組,包括對象自身的(不含繼承的)所有可枚舉屬性(不含 Symbol 屬性)的鍵名。
**(3)Object.getOwnPropertyNames(obj)**
`Object.getOwnPropertyNames`返回一個數組,包含對象自身的所有屬性(不含 Symbol 屬性,但是包括不可枚舉屬性)的鍵名。
**(4)Object.getOwnPropertySymbols(obj)**
`Object.getOwnPropertySymbols`返回一個數組,包含對象自身的所有 Symbol 屬性的鍵名。
**(5)Reflect.ownKeys(obj)**
`Reflect.ownKeys`返回一個數組,包含對象自身的(不含繼承的)所有鍵名,不管鍵名是 Symbol 或字符串,也不管是否可枚舉。
以上的 5 種方法遍歷對象的鍵名,都遵守同樣的屬性遍歷的次序規則。
- 首先遍歷所有數值鍵,按照數值升序排列。
- 其次遍歷所有字符串鍵,按照加入時間升序排列。
- 最后遍歷所有 Symbol 鍵,按照加入時間升序排列。
```javascript
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]
```
上面代碼中,`Reflect.ownKeys`方法返回一個數組,包含了參數對象的所有屬性。這個數組的屬性次序是這樣的,首先是數值屬性`2`和`10`,其次是字符串屬性`b`和`a`,最后是 Symbol 屬性。
## super 關鍵字
我們知道,`this`關鍵字總是指向函數所在的當前對象,ES6 又新增了另一個類似的關鍵字`super`,指向當前對象的原型對象。
```javascript
const proto = {
foo: 'hello'
};
const obj = {
foo: 'world',
find() {
return super.foo;
}
};
Object.setPrototypeOf(obj, proto);
obj.find() // "hello"
```
上面代碼中,對象`obj.find()`方法之中,通過`super.foo`引用了原型對象`proto`的`foo`屬性。
注意,`super`關鍵字表示原型對象時,只能用在對象的方法之中,用在其他地方都會報錯。
```javascript
// 報錯
const obj = {
foo: super.foo
}
// 報錯
const obj = {
foo: () => super.foo
}
// 報錯
const obj = {
foo: function () {
return super.foo
}
}
```
上面三種`super`的用法都會報錯,因為對于 JavaScript 引擎來說,這里的`super`都沒有用在對象的方法之中。第一種寫法是`super`用在屬性里面,第二種和第三種寫法是`super`用在一個函數里面,然后賦值給`foo`屬性。目前,只有對象方法的簡寫法可以讓 JavaScript 引擎確認,定義的是對象的方法。
JavaScript 引擎內部,`super.foo`等同于`Object.getPrototypeOf(this).foo`(屬性)或`Object.getPrototypeOf(this).foo.call(this)`(方法)。
```javascript
const proto = {
x: 'hello',
foo() {
console.log(this.x);
},
};
const obj = {
x: 'world',
foo() {
super.foo();
}
}
Object.setPrototypeOf(obj, proto);
obj.foo() // "world"
```
上面代碼中,`super.foo`指向原型對象`proto`的`foo`方法,但是綁定的`this`卻還是當前對象`obj`,因此輸出的就是`world`。
## 對象的擴展運算符
《數組的擴展》一章中,已經介紹過擴展運算符(`...`)。ES2018 將這個運算符[引入](https://github.com/sebmarkbage/ecmascript-rest-spread)了對象。
### 解構賦值
對象的解構賦值用于從一個對象取值,相當于將目標對象自身的所有可遍歷的(enumerable)、但尚未被讀取的屬性,分配到指定的對象上面。所有的鍵和它們的值,都會拷貝到新對象上面。
```javascript
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }
```
上面代碼中,變量`z`是解構賦值所在的對象。它獲取等號右邊的所有尚未讀取的鍵(`a`和`b`),將它們連同值一起拷貝過來。
由于解構賦值要求等號右邊是一個對象,所以如果等號右邊是`undefined`或`null`,就會報錯,因為它們無法轉為對象。
```javascript
let { ...z } = null; // 運行時錯誤
let { ...z } = undefined; // 運行時錯誤
```
解構賦值必須是最后一個參數,否則會報錯。
```javascript
let { ...x, y, z } = someObject; // 句法錯誤
let { x, ...y, ...z } = someObject; // 句法錯誤
```
上面代碼中,解構賦值不是最后一個參數,所以會報錯。
注意,解構賦值的拷貝是淺拷貝,即如果一個鍵的值是復合類型的值(數組、對象、函數)、那么解構賦值拷貝的是這個值的引用,而不是這個值的副本。
```javascript
let obj = { a: { b: 1 } };
let { ...x } = obj;
obj.a.b = 2;
x.a.b // 2
```
上面代碼中,`x`是解構賦值所在的對象,拷貝了對象`obj`的`a`屬性。`a`屬性引用了一個對象,修改這個對象的值,會影響到解構賦值對它的引用。
另外,擴展運算符的解構賦值,不能復制繼承自原型對象的屬性。
```javascript
let o1 = { a: 1 };
let o2 = { b: 2 };
o2.__proto__ = o1;
let { ...o3 } = o2;
o3 // { b: 2 }
o3.a // undefined
```
上面代碼中,對象`o3`復制了`o2`,但是只復制了`o2`自身的屬性,沒有復制它的原型對象`o1`的屬性。
下面是另一個例子。
```javascript
const o = Object.create({ x: 1, y: 2 });
o.z = 3;
let { x, ...newObj } = o;
let { y, z } = newObj;
x // 1
y // undefined
z // 3
```
上面代碼中,變量`x`是單純的解構賦值,所以可以讀取對象`o`繼承的屬性;變量`y`和`z`是擴展運算符的解構賦值,只能讀取對象`o`自身的屬性,所以變量`z`可以賦值成功,變量`y`取不到值。ES6 規定,變量聲明語句之中,如果使用解構賦值,擴展運算符后面必須是一個變量名,而不能是一個解構賦值表達式,所以上面代碼引入了中間變量`newObj`,如果寫成下面這樣會報錯。
```javascript
let { x, ...{ y, z } } = o;
// SyntaxError: ... must be followed by an identifier in declaration contexts
```
解構賦值的一個用處,是擴展某個函數的參數,引入其他操作。
```javascript
function baseFunction({ a, b }) {
// ...
}
function wrapperFunction({ x, y, ...restConfig }) {
// 使用 x 和 y 參數進行操作
// 其余參數傳給原始函數
return baseFunction(restConfig);
}
```
上面代碼中,原始函數`baseFunction`接受`a`和`b`作為參數,函數`wrapperFunction`在`baseFunction`的基礎上進行了擴展,能夠接受多余的參數,并且保留原始函數的行為。
### 擴展運算符
對象的擴展運算符(`...`)用于取出參數對象的所有可遍歷屬性,拷貝到當前對象之中。
```javascript
let z = { a: 3, b: 4 };
let n = { ...z };
n // { a: 3, b: 4 }
```
由于數組是特殊的對象,所以對象的擴展運算符也可以用于數組。
```javascript
let foo = { ...['a', 'b', 'c'] };
foo
// {0: "a", 1: "b", 2: "c"}
```
如果擴展運算符后面是一個空對象,則沒有任何效果。
```javascript
{...{}, a: 1}
// { a: 1 }
```
如果擴展運算符后面不是對象,則會自動將其轉為對象。
```javascript
// 等同于 {...Object(1)}
{...1} // {}
```
上面代碼中,擴展運算符后面是整數`1`,會自動轉為數值的包裝對象`Number{1}`。由于該對象沒有自身屬性,所以返回一個空對象。
下面的例子都是類似的道理。
```javascript
// 等同于 {...Object(true)}
{...true} // {}
// 等同于 {...Object(undefined)}
{...undefined} // {}
// 等同于 {...Object(null)}
{...null} // {}
```
但是,如果擴展運算符后面是字符串,它會自動轉成一個類似數組的對象,因此返回的不是空對象。
```javascript
{...'hello'}
// {0: "h", 1: "e", 2: "l", 3: "l", 4: "o"}
```
對象的擴展運算符等同于使用`Object.assign()`方法。
```javascript
let aClone = { ...a };
// 等同于
let aClone = Object.assign({}, a);
```
上面的例子只是拷貝了對象實例的屬性,如果想完整克隆一個對象,還拷貝對象原型的屬性,可以采用下面的寫法。
```javascript
// 寫法一
const clone1 = {
__proto__: Object.getPrototypeOf(obj),
...obj
};
// 寫法二
const clone2 = Object.assign(
Object.create(Object.getPrototypeOf(obj)),
obj
);
// 寫法三
const clone3 = Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
)
```
上面代碼中,寫法一的`__proto__`屬性在非瀏覽器的環境不一定部署,因此推薦使用寫法二和寫法三。
擴展運算符可以用于合并兩個對象。
```javascript
let ab = { ...a, ...b };
// 等同于
let ab = Object.assign({}, a, b);
```
如果用戶自定義的屬性,放在擴展運算符后面,則擴展運算符內部的同名屬性會被覆蓋掉。
```javascript
let aWithOverrides = { ...a, x: 1, y: 2 };
// 等同于
let aWithOverrides = { ...a, ...{ x: 1, y: 2 } };
// 等同于
let x = 1, y = 2, aWithOverrides = { ...a, x, y };
// 等同于
let aWithOverrides = Object.assign({}, a, { x: 1, y: 2 });
```
上面代碼中,`a`對象的`x`屬性和`y`屬性,拷貝到新對象后會被覆蓋掉。
這用來修改現有對象部分的屬性就很方便了。
```javascript
let newVersion = {
...previousVersion,
name: 'New Name' // Override the name property
};
```
上面代碼中,`newVersion`對象自定義了`name`屬性,其他屬性全部復制自`previousVersion`對象。
如果把自定義屬性放在擴展運算符前面,就變成了設置新對象的默認屬性值。
```javascript
let aWithDefaults = { x: 1, y: 2, ...a };
// 等同于
let aWithDefaults = Object.assign({}, { x: 1, y: 2 }, a);
// 等同于
let aWithDefaults = Object.assign({ x: 1, y: 2 }, a);
```
與數組的擴展運算符一樣,對象的擴展運算符后面可以跟表達式。
```javascript
const obj = {
...(x > 1 ? {a: 1} : {}),
b: 2,
};
```
擴展運算符的參數對象之中,如果有取值函數`get`,這個函數是會執行的。
```javascript
let a = {
get x() {
throw new Error('not throw yet');
}
}
let aWithXGetter = { ...a }; // 報錯
```
上面例子中,取值函數`get`在擴展`a`對象時會自動執行,導致報錯。
## 鏈判斷運算符
編程實務中,如果讀取對象內部的某個屬性,往往需要判斷一下該對象是否存在。比如,要讀取`message.body.user.firstName`,安全的寫法是寫成下面這樣。
```javascript
// 錯誤的寫法
const firstName = message.body.user.firstName;
// 正確的寫法
const firstName = (message
&& message.body
&& message.body.user
&& message.body.user.firstName) || 'default';
```
上面例子中,`firstName`屬性在對象的第四層,所以需要判斷四次,每一層是否有值。
三元運算符`?:`也常用于判斷對象是否存在。
```javascript
const fooInput = myForm.querySelector('input[name=foo]')
const fooValue = fooInput ? fooInput.value : undefined
```
上面例子中,必須先判斷`fooInput`是否存在,才能讀取`fooInput.value`。
這樣的層層判斷非常麻煩,因此 [ES2020](https://github.com/tc39/proposal-optional-chaining) 引入了“鏈判斷運算符”(optional chaining operator)`?.`,簡化上面的寫法。
```javascript
const firstName = message?.body?.user?.firstName || 'default';
const fooValue = myForm.querySelector('input[name=foo]')?.value
```
上面代碼使用了`?.`運算符,直接在鏈式調用的時候判斷,左側的對象是否為`null`或`undefined`。如果是的,就不再往下運算,而是返回`undefined`。
下面是判斷對象方法是否存在,如果存在就立即執行的例子。
```javascript
iterator.return?.()
```
上面代碼中,`iterator.return`如果有定義,就會調用該方法,否則`iterator.return`直接返回`undefined`,不再執行`?.`后面的部分。
對于那些可能沒有實現的方法,這個運算符尤其有用。
```javascript
if (myForm.checkValidity?.() === false) {
// 表單校驗失敗
return;
}
```
上面代碼中,老式瀏覽器的表單可能沒有`checkValidity`這個方法,這時`?.`運算符就會返回`undefined`,判斷語句就變成了`undefined === false`,所以就會跳過下面的代碼。
鏈判斷運算符有三種用法。
- `obj?.prop` // 對象屬性
- `obj?.[expr]` // 同上
- `func?.(...args)` // 函數或對象方法的調用
下面是`obj?.[expr]`用法的一個例子。
```bash
let hex = "#C0FFEE".match(/#([A-Z]+)/i)?.[1];
```
上面例子中,字符串的`match()`方法,如果沒有發現匹配會返回`null`,如果發現匹配會返回一個數組,`?.`運算符起到了判斷作用。
下面是`?.`運算符常見形式,以及不使用該運算符時的等價形式。
```javascript
a?.b
// 等同于
a == null ? undefined : a.b
a?.[x]
// 等同于
a == null ? undefined : a[x]
a?.b()
// 等同于
a == null ? undefined : a.b()
a?.()
// 等同于
a == null ? undefined : a()
```
上面代碼中,特別注意后兩種形式,如果`a?.b()`里面的`a.b`不是函數,不可調用,那么`a?.b()`是會報錯的。`a?.()`也是如此,如果`a`不是`null`或`undefined`,但也不是函數,那么`a?.()`會報錯。
使用這個運算符,有幾個注意點。
(1)短路機制
`?.`運算符相當于一種短路機制,只要不滿足條件,就不再往下執行。
```javascript
a?.[++x]
// 等同于
a == null ? undefined : a[++x]
```
上面代碼中,如果`a`是`undefined`或`null`,那么`x`不會進行遞增運算。也就是說,鏈判斷運算符一旦為真,右側的表達式就不再求值。
(2)delete 運算符
```javascript
delete a?.b
// 等同于
a == null ? undefined : delete a.b
```
上面代碼中,如果`a`是`undefined`或`null`,會直接返回`undefined`,而不會進行`delete`運算。
(3)括號的影響
如果屬性鏈有圓括號,鏈判斷運算符對圓括號外部沒有影響,只對圓括號內部有影響。
```javascript
(a?.b).c
// 等價于
(a == null ? undefined : a.b).c
```
上面代碼中,`?.`對圓括號外部沒有影響,不管`a`對象是否存在,圓括號后面的`.c`總是會執行。
一般來說,使用`?.`運算符的場合,不應該使用圓括號。
(4)報錯場合
以下寫法是禁止的,會報錯。
```javascript
// 構造函數
new a?.()
new a?.b()
// 鏈判斷運算符的右側有模板字符串
a?.`{b}`
a?.b`{c}`
// 鏈判斷運算符的左側是 super
super?.()
super?.foo
// 鏈運算符用于賦值運算符左側
a?.b = c
```
(5)右側不得為十進制數值
為了保證兼容以前的代碼,允許`foo?.3:0`被解析成`foo ? .3 : 0`,因此規定如果`?.`后面緊跟一個十進制數字,那么`?.`不再被看成是一個完整的運算符,而會按照三元運算符進行處理,也就是說,那個小數點會歸屬于后面的十進制數字,形成一個小數。
## Null 判斷運算符
讀取對象屬性的時候,如果某個屬性的值是`null`或`undefined`,有時候需要為它們指定默認值。常見做法是通過`||`運算符指定默認值。
```javascript
const headerText = response.settings.headerText || 'Hello, world!';
const animationDuration = response.settings.animationDuration || 300;
const showSplashScreen = response.settings.showSplashScreen || true;
```
上面的三行代碼都通過`||`運算符指定默認值,但是這樣寫是錯的。開發者的原意是,只要屬性的值為`null`或`undefined`,默認值就會生效,但是屬性的值如果為空字符串或`false`或`0`,默認值也會生效。
為了避免這種情況,[ES2020](https://github.com/tc39/proposal-nullish-coalescing) 引入了一個新的 Null 判斷運算符`??`。它的行為類似`||`,但是只有運算符左側的值為`null`或`undefined`時,才會返回右側的值。
```javascript
const headerText = response.settings.headerText ?? 'Hello, world!';
const animationDuration = response.settings.animationDuration ?? 300;
const showSplashScreen = response.settings.showSplashScreen ?? true;
```
上面代碼中,默認值只有在左側屬性值為`null`或`undefined`時,才會生效。
這個運算符的一個目的,就是跟鏈判斷運算符`?.`配合使用,為`null`或`undefined`的值設置默認值。
```javascript
const animationDuration = response.settings?.animationDuration ?? 300;
```
上面代碼中,如果`response.settings`是`null`或`undefined`,或者`response.settings.animationDuration`是`null`或`undefined`,就會返回默認值300。也就是說,這一行代碼包括了兩級屬性的判斷。
這個運算符很適合判斷函數參數是否賦值。
```javascript
function Component(props) {
const enable = props.enabled ?? true;
// …
}
```
上面代碼判斷`props`參數的`enabled`屬性是否賦值,基本等同于下面的寫法。
```javascript
function Component(props) {
const {
enabled: enable = true,
} = props;
// …
}
```
`??`有一個運算優先級問題,它與`&&`和`||`的優先級孰高孰低。現在的規則是,如果多個邏輯運算符一起使用,必須用括號表明優先級,否則會報錯。
```javascript
// 報錯
lhs && middle ?? rhs
lhs ?? middle && rhs
lhs || middle ?? rhs
lhs ?? middle || rhs
```
上面四個表達式都會報錯,必須加入表明優先級的括號。
```javascript
(lhs && middle) ?? rhs;
lhs && (middle ?? rhs);
(lhs ?? middle) && rhs;
lhs ?? (middle && rhs);
(lhs || middle) ?? rhs;
lhs || (middle ?? rhs);
(lhs ?? middle) || rhs;
lhs ?? (middle || rhs);
```
- 前言
- ECMAScript 6簡介
- let 和 const 命令
- 變量的解構賦值
- 字符串的擴展
- 字符串的新增方法
- 正則的擴展
- 數值的擴展
- 函數的擴展
- 數組的擴展
- 對象的擴展
- 對象的新增方法
- Symbol
- Set 和 Map 數據結構
- Proxy
- Reflect
- Promise 對象
- Iterator 和 for...of 循環
- Generator 函數的語法
- Generator 函數的異步應用
- async 函數
- Class 的基本語法
- Class 的繼承
- Module 的語法
- Module 的加載實現
- 編程風格
- 讀懂規格
- 異步遍歷器
- ArrayBuffer
- 最新提案
- Decorator
- 參考鏈接