作者 | 阮一峰
對(duì)象(object)是 JavaScript 最重要的數(shù)據(jù)結(jié)構(gòu)。ES6 對(duì)它進(jìn)行了重大升級(jí),本章介紹數(shù)據(jù)結(jié)構(gòu)本身的改變,下一章介紹Object對(duì)象的新增方法。1、屬性的簡(jiǎn)潔表示法
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!";
}
};
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ù)使用。2、屬性名表達(dá)式
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
};
let lastWord = 'last word';
const a = {
'first word': 'hello',
[lastWord]: 'world'
};
a['first word'] // "hello"
a[lastWord] // "world"
a['last word'] // "world"
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]屬性。3、方法的 name 屬性
函數(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沒有。4、屬性的可枚舉性和遍歷
可枚舉性
對(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的屬性。for...in循環(huán):只遍歷對(duì)象自身的和繼承的可枚舉的屬性。
Object.keys():返回對(duì)象自身的所有可枚舉的屬性的鍵名。
JSON.stringify():只串行化對(duì)象自身的可枚舉的屬性。
Object.assign(): 忽略enumerable為false的屬性,只拷貝對(duì)象自身的可枚舉的屬性。
這四個(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ì)象的屬性。for...in循環(huán)遍歷對(duì)象自身的和繼承的可枚舉屬性(不含 Symbol 屬性)。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 屬性的鍵名。Reflect.ownKeys返回一個(gè)數(shù)組,包含對(duì)象自身的(不含繼承的)所有鍵名,不管鍵名是 Symbol 或字符串,也不管是否可枚舉。以上的 5 種方法遍歷對(duì)象的鍵名,都遵守同樣的屬性遍歷的次序規(guī)則。首先遍歷所有數(shù)值鍵,按照數(shù)值升序排列。
其次遍歷所有字符串鍵,按照加入時(shí)間升序排列。
最后遍歷所有 Symbol 鍵,按照加入時(shí)間升序排列。
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 屬性。5、super 關(guān)鍵字
我們知道,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都沒有用在對(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。6、對(duì)象的擴(kuò)展運(yùn)算符
《數(shù)組的擴(kuò)展》一章中,已經(jīng)介紹過(guò)擴(kuò)展運(yùn)算符(...)。ES2018 將這個(gè)運(yùn)算符引入了對(duì)象。解構(gòu)賦值
對(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自身的屬性,沒有復(fù)制它的原型對(duì)象o1的屬性。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ù)的行為。擴(kuò)展運(yùn)算符
對(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ì)象,則沒有任何效果。如果擴(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ì)象沒有自身屬性,所以返回一個(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ò)。7、鏈判斷運(yùn)算符
編程實(shí)務(wù)中,如果讀取對(duì)象內(nèi)部的某個(gè)屬性,往往需要判斷一下該對(duì)象是否存在。比如,要讀取message.body.user.firstName,安全的寫法是寫成下面這樣。// 錯(cuò)誤的寫法
const firstName = message.body.user.firstName;
// 正確的寫法
const firstName = (message
&& message.body
&& message.body.user
&& message.body.user.firstName) || 'default';
上面例子中,firstName屬性在對(duì)象的第四層,所以需要判斷四次,每一層是否有值。三元運(yùn)算符?:也常用于判斷對(duì)象是否存在。const fooInput = myForm.querySelector('input[name=foo]')
const fooValue = fooInput ? fooInput.value : undefined
上面例子中,必須先判斷fooInput是否存在,才能讀取fooInput.value。這樣的層層判斷非常麻煩,因此?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。下面是判斷對(duì)象方法是否存在,如果存在就立即執(zhí)行的例子。上面代碼中,iterator.return如果有定義,就會(huì)調(diào)用該方法,否則iterator.return直接返回undefined,不再執(zhí)行?.后面的部分。對(duì)于那些可能沒有實(shí)現(xiàn)的方法,這個(gè)運(yùn)算符尤其有用。if (myForm.checkValidity?.() === false) {
// 表單校驗(yàn)失敗
return;
}
上面代碼中,老式瀏覽器的表單可能沒有checkValidity這個(gè)方法,這時(shí)?.運(yùn)算符就會(huì)返回undefined,判斷語(yǔ)句就變成了undefined === false,所以就會(huì)跳過(guò)下面的代碼。下面是obj?.[expr]用法的一個(gè)例子。let hex = "#C0FFEE".match(/#([A-Z]+)/i)?.[1];
上面例子中,字符串的match()方法,如果沒有發(fā)現(xiàn)匹配會(huì)返回null,如果發(fā)現(xiàn)匹配會(huì)返回一個(gè)數(shù)組,?.運(yùn)算符起到了判斷作用。下面是?.運(yù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)。?.運(yùn)算符相當(dāng)于一種短路機(jī)制,只要不滿足條件,就不再往下執(zhí)行。a?.[++x]
// 等同于
a == null ? undefined : a[++x]
上面代碼中,如果a是undefined或null,那么x不會(huì)進(jìn)行遞增運(yùn)算。也就是說(shuō),鏈判斷運(yùn)算符一旦為真,右側(cè)的表達(dá)式就不再求值。delete a?.b
// 等同于
a == null ? undefined : delete a.b
上面代碼中,如果a是undefined或null,會(huì)直接返回undefined,而不會(huì)進(jìn)行delete運(yùn)算。如果屬性鏈有圓括號(hào),鏈判斷運(yùn)算符對(duì)圓括號(hào)外部沒有影響,只對(duì)圓括號(hào)內(nèi)部有影響。(a?.b).c
// 等價(jià)于
(a == null ? undefined : a.b).c
上面代碼中,?.對(duì)圓括號(hào)外部沒有影響,不管a對(duì)象是否存在,圓括號(hào)后面的.c總是會(huì)執(zhí)行。一般來(lái)說(shuō),使用?.運(yùn)算符的場(chǎng)合,不應(yīng)該使用圓括號(hào)。以下寫法是禁止的,會(huì)報(bào)錯(cuò)。// 構(gòu)造函數(shù)
new a?.()
new a?.b()
// 鏈判斷運(yùn)算符的右側(cè)有模板字符串
a?.`{b}`
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ù)。8、Null 判斷運(yùn)算符
讀取對(duì)象屬性的時(shí)候,如果某個(gè)屬性的值是null或undefined,有時(shí)候需要為它們指定默認(rè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ò)的。開發(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)值只有在左側(cè)屬性值為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,或者response.settings.animationDuration是null或undefined,就會(huì)返回默認(rèn)值300。也就是說(shuō),這一行代碼包括了兩級(jí)屬性的判斷。這個(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í)孰高孰低。現(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);