ES6 對(duì)象的擴(kuò)展

2022-04-01 17:37 更新

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!";
  }
};

下面是一個(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ù)使用。

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
};

下面是另一個(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] 屬性。

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 屬性,就通過“可枚舉性”,從而避免被 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ì)讓問題復(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ī)則。

  • 首先遍歷所有數(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()方法之中,通過 super.foo引用了原型對(duì)象protofoo 屬性。

注意, 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)介紹過擴(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 ),將它們連同值一起拷貝過來(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 的屬性。

下面是另一個(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ù)的行為。

擴(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ì)象,則沒有任何效果。

{...{}, 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ì)象沒有自身屬性,所以返回一個(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í)際編程中,如果讀取對(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)算符有三種用法。

  • obj?.prop // 對(duì)象屬性
  • obj?.[expr] // 同上
  • func?.(...args) // 函數(shù)或?qū)ο蠓椒ǖ恼{(diào)用

下面是判斷對(duì)象方法是否存在,如果存在就立即執(zhí)行的例子。

iterator.return?.()

上面代碼中,iterator.return如果有定義,就會(huì)調(diào)用該方法,否則直接返回 undefined 。

對(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ì)跳過下面的代碼。

下面是這個(gè)運(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)。

(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)外部沒有影響,只對(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)。

(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ù)。

8. Null 判斷運(yùn)算符

讀取對(duì)象屬性的時(shí)候,如果某個(gè)屬性的值是 null 或 undefined ,有時(shí)候需要為它們指定默認(rèn)值。常見做法是通過 || 運(yùn)算符指定默認(rèn)值。

const headerText = response.settings.headerText || 'Hello, world!';
const animationDuration = response.settings.animationDuration || 300;
const showSplashScreen = response.settings.showSplashScreen || true;

上面的三行代碼都通過 || 運(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)值只有在屬性值為 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í)問題,它與 && 和 || 的優(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)潔。
以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)