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