天天看點

ES6學習筆記(八)ES6

補充學習一下 ES6 對象擴充 方面的知識,包含以下内容:

1、對象屬性的簡潔表示法,屬性名可以使用表達式(方括号表示);

2、對象方法的name屬性,遇到setter和getter時需要通過Object.getOwnPropertyDescriptor方法取得方法的屬性的描述對象,再去得到name屬性,除此之外還有bind建立和Function構造函數兩種特殊情況;

3、屬性的周遊(結合Symbol更新了規則);

4、super關鍵字;

5、擴充運算符;

6、?. 和 ?? 運算符。

文章目錄

  • ES6
    • 對象的擴充
      • 1、屬性的簡潔表示法
      • 2、屬性名表達式
      • 3、方法的 name 屬性
        • 方法的name屬性的兩種特殊情況
      • 4、屬性的可枚舉性和周遊
        • 可枚舉性
        • 屬性的周遊
          • (1)for...in
          • (2)Object.keys(obj)
          • (3)Object.getOwnPropertyNames(obj)
          • (4)Object.getOwnPropertySymbols(obj)
          • (5)Reflect.ownKeys(obj)
      • 5、super 關鍵字
      • 6、對象的擴充運算符
        • 解構指派
        • 擴充運算符
      • 7、鍊判斷運算符
        • 注意點
          • (1)短路機制
          • (2)delete 運算符
          • (3)括号的影響
          • (4)報錯場合
          • (5)右側不得為十進制數值
      • 8、Null 判斷運算符

ES6

對象的擴充

1、屬性的簡潔表示法

ES6 允許在大括号裡面,直接寫入變量和函數,作為對象的屬性和方法。這樣的書寫更加簡潔。下面是一個實際的例子。

let birth = '2000/01/01';

const Person = {

  name: '張三',

  //等同于birth: birth
  birth,

  // 等同于hello: function ()...
  hello() { console.log('我的名字是', this.name); }

};
           

這種寫法用于函數的傳回值,将會非常友善。CommonJS 子產品輸出一組變量,就非常合适使用簡潔寫法。

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),事實上也是采用這種寫法。

const cart = {
  _wheels: 4,

  get wheels () {
    return this._wheels;
  },

  set wheels (value) {
    if (value < this._wheels) {
      throw new Error('數值太小了!');
    }
    this._wheels = value;
  }
}
           

注意,簡寫的對象方法不能用作構造函數,會報錯。

const obj = {
  f() {
    this.foo = 'bar';
  }
};

new obj.f() // 報錯
           

上面代碼中,f是一個簡寫的對象方法,是以obj.f不能當作構造函數使用。

2、屬性名表達式

JavaScript 定義對象的屬性,有兩種方法。

// 方法一
obj.foo = true;

// 方法二
obj['a' + 'bc'] = 123;
           

上面代碼的方法一是直接用辨別符作為屬性名,方法二是用表達式作為屬性名,這時要将表達式放在方括号之内。

但是,如果使用字面量方式定義對象(使用大括号),在 ES5 中隻能使用方法一(辨別符)定義屬性。

var obj = {
  foo: true,
  abc: 123
};
           

ES6 允許字面量定義對象時,用方法二(表達式)作為對象的屬性名,即把表達式放在方括号内。

let propKey = 'foo';

let obj = {
  [propKey]: true,
  ['a' + 'bc']: 123
};
           

下面是另一個例子。同時,表達式還可以用于定義方法名。

let lastWord = 'last word';

const a = {
  'first word': 'hello',
  [lastWord]: 'world',
  ['h' + 'ello']() {
    return 'hi';
  }
};

a['first word'] // "hello"
a[lastWord] // "world"
a['last word'] // "world"
a.hello() // hi
           

注意,屬性名表達式與簡潔表示法,不能同時使用,會報錯。

// 報錯
const foo = 'bar';
const bar = 'abc';
const baz = { [foo] };

// 正确
const foo = 'bar';
const baz = { [foo]: 'abc'};
           

注意,屬性名表達式如果是一個對象,預設情況下會自動将對象轉為字元串[object Object],這一點要特别小心。

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]屬性。

3、方法的 name 屬性

函數的name屬性,傳回函數名。對象方法也是函數,是以也有name屬性。

const person = {
  sayName() {
    console.log('hello!');
  },
};

person.sayName.name   // "sayName"
           

如果對象的方法使用了取值函數(getter)和存值函數(setter),則name屬性不是在該方法上面(不存在name屬性),而是該方法的屬性的描述對象(通過Object.getOwnPropertyDescriptor得到對象的方法的描述對象)的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');
// obj.foo的描述對象

descriptor.get.name // "get foo"
descriptor.set.name // "set foo"
           

方法的name屬性的兩種特殊情況

方法的name屬性還有兩種特殊情況:

1、bind方法創造的函數,name屬性傳回bound加上原函數的名字;

2、Function構造函數創造的函數,name屬性傳回anonymous(匿名)。

(new Function()).name // "anonymous"

var doSomething = function() {
  // ...
};
doSomething.bind().name // "bound doSomething"
           

如果對象的方法是一個 Symbol 值,那麼name屬性傳回的是這個 Symbol 值的描述。

const key1 = Symbol('description');
const key2 = Symbol();
let obj = {
  [key1]() {},
  [key2]() {},
};
obj[key1].name // "[description]"
obj[key2].name // ""
           

上面代碼中,key1對應的 Symbol 值有描述,key2沒有。

4、屬性的可枚舉性和周遊

可枚舉性

對象的每個屬性都有一個描述對象(Descriptor),用來控制該屬性的行為。Object.getOwnPropertyDescriptor方法可以擷取該屬性的描述對象。

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周遊到。

Object.getOwnPropertyDescriptor(Object.prototype, 'toString').enumerable
// false

Object.getOwnPropertyDescriptor([], 'length').enumerable
// false
           

上面代碼中,toString和length屬性的enumerable都是false,是以for…in不會周遊到這兩個繼承自原型的屬性。

另外,ES6 規定,所有 Class 的原型的方法都是不可枚舉的。

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 種方法周遊對象的鍵名,都遵守同樣的屬性周遊的次序規則。

1、首先周遊所有數值鍵,按照數值升序排列。

2、其次周遊所有字元串鍵,按照加入時間升序排列。

3、最後周遊所有 Symbol 鍵,按照加入時間升序排列。

Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]
           

上面代碼中,Reflect.ownKeys方法傳回一個數組,包含了參數對象的所有屬性。這個數組的屬性次序是這樣的,首先是數值屬性2和10,其次是字元串屬性b和a,最後是 Symbol 屬性。

5、super 關鍵字

我們知道,this關鍵字總是指向函數所在的目前對象,ES6 又新增了另一個類似的關鍵字super,指向目前對象的原型對象。

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關鍵字表示原型對象時,隻能用在對象的方法之中,用在其他地方都會報錯。

// 報錯
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)(方法)。

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。

6、對象的擴充運算符

解構指派

對象的解構指派之前已經了解過了,隻做簡單叙述。

對象的解構指派用于從一個對象取值,相當于将目标對象自身的所有可周遊的(enumerable)、但尚未被讀取的屬性,配置設定到指定的對象上面。所有的鍵和它們的值,都會拷貝到新對象上面。

let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }
           

需要注意的有:

1、等号右邊必須是一個對象,如果是undefined或者null,就會報錯,因為它們無法轉為對象。

2、等号左邊,擴充運算符形式的解構指派必須是最後一個參數,否則會報錯。

3、解構指派的拷貝是淺拷貝,是以當一個鍵的值是引用類型值(數組、對象、函數、Date等等),那麼拷貝到的将是這個值的引用,而非這個值的副本。

擴充運算符

對象的擴充運算符用于取出參數對象的所有可周遊屬性(enumerable==true),拷貝到目前對象之中。同樣,數組是特殊的對象,是以也能應用于數組。

如果擴充運算符後面跟的不是對象,則會自動轉為對象。比如說擴充運算符後面跟了字面量的整數1,會轉為數值的包裝對象Number(1),但因為該對象沒有自身屬性,是以會傳回一個空對象。

加入是字元串,則會轉為類數組對象:

{...'hello'}
// {0: "h", 1: "e", 2: "l", 3: "l", 4: "o"}
           

對象的擴充運算符屬于淺拷貝,其效果等同于Object.assign()方法:

let aClone = { ...a };
// 等同于
let aClone = Object.assign({}, a);
           

上面的寫法隻是拷貝了對象執行個體的屬性,如果想完整克隆一個對象,還拷貝對象原型的屬性,可以采用以下三種寫法:

// 寫法一

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__屬性在非浏覽器不一定部署,是以推薦使用寫法二和三。

7、鍊判斷運算符

程式設計實務中,如果讀取對象内部的某個屬性,往往需要判斷一下該對象是否存在。比如,要讀取message.body.user.firstName,安全的寫法是寫成下面這樣。

const firstName = (message
  && message.body
  && message.body.user
  && message.body.user.firstName) || 'default';
           

或者使用三元運算符?:,判斷一個對象是否存在。

const fooInput = myForm.querySelector('input[name=foo]')
const fooValue = fooInput ? fooInput.value : undefined
           

這樣的層層判斷非常麻煩,是以 ES2020 引入了“鍊判斷運算符”(optional chaining operator)?.,簡化上面的寫法。

const firstName = message?.body?.user?.firstName || 'default';
const fooValue = myForm.querySelector('input[name=foo]')?.value
           

上面代碼使用了?.運算符,直接在鍊式調用的時候判斷,左側的對象是否為null或undefined。如果是的,就不再往下運算,而是傳回undefined。

鍊判斷運算符有三種用法。

obj?.prop // 對象屬性
obj?.[expr] // 同上
func?.(...args) // 函數或對象方法的調用
           

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

上面代碼中,iterator.return如果有定義,就會調用該方法,否則直接傳回undefined。

對于那些可能沒有實作的方法,這個運算符尤其有用。

if (myForm.checkValidity?.() === false) {
  // 表單校驗失敗
  return;
}
           

上面代碼中,老式浏覽器的表單可能沒有checkValidity這個方法,這時?.運算符就會傳回undefined,判斷語句就變成了undefined === false,是以就會跳過下面的代碼。

下面是這個運算符常見的使用形式,以及不使用該運算符時的等價形式。

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)短路機制
a?.[++x]
// 等同于
a == null ? undefined : a[++x]
           

上面代碼中,如果a是undefined或null,那麼x不會進行遞增運算。也就是說,鍊判斷運算符一旦為真,右側的表達式就不再求值。

(2)delete 運算符
delete a?.b
// 等同于
a == null ? undefined : delete a.b
           

上面代碼中,如果a是undefined或null,會直接傳回undefined,而不會進行delete運算。

(3)括号的影響

如果屬性鍊有圓括号,鍊判斷運算符對圓括号外部沒有影響,隻對圓括号内部有影響。

(a?.b).c
// 等價于
(a == null ? undefined : a.b).c
           

上面代碼中,?.對圓括号外部沒有影響,不管a對象是否存在,圓括号後面的.c總是會執行。

一般來說,使用?.運算符的場合,不應該使用圓括号。

(4)報錯場合

以下寫法是禁止的,會報錯。

// 構造函數
new a?.()
new a?.b()

// 鍊判斷運算符的右側有模闆字元串
a?.`{b}`
a?.b`{c}`

// 鍊判斷運算符的左側是 super
super?.()
super?.foo

// 鍊運算符用于指派運算符左側
a?.b = c
           
(5)右側不得為十進制數值

為了保證相容以前的代碼,允許foo?.3:0被解析成foo ? .3 : 0,是以規定如果?.後面緊跟一個十進制數字,那麼?.不再被看成是一個完整的運算符,而會按照三元運算符進行處理,也就是說,那個小數點會歸屬于後面的十進制數字,形成一個小數。

8、Null 判斷運算符

讀取對象屬性的時候,如果某個屬性的值是null或undefined,有時候需要為它們指定預設值。常見做法是通過||運算符指定預設值。

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

上面的三行代碼都通過||運算符指定預設值,但是這樣寫是錯的。開發者的原意是,隻要屬性的值為null或undefined,預設值就會生效,但是屬性的值如果為空字元串或false或0,預設值也會生效。

為了避免這種情況,ES2020 引入了一個新的 Null 判斷運算符 ??。它的行為類似||,但是隻有運算符左側的值為null或undefined時,才會傳回右側的值。

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

上面代碼中,預設值隻有在屬性值為null或undefined時,才會生效。

這個運算符的一個目的,就是跟鍊判斷運算符 ?. 配合使用,為null或undefined的值設定預設值。

上面代碼中,response.settings如果是null或undefined,就會傳回預設值300。

這個運算符很适合判斷函數參數是否指派。

function Component(props) {
  const enable = props.enabled ?? true;
  // …
}
           

上面代碼判斷props參數的enabled屬性是否指派,等同于下面的寫法。

function Component(props) {
  const {
    enabled: enable = true,
  } = props;
  // …
}
           

??有一個運算優先級問題,它與&&和||的優先級孰高孰低。現在的規則是,如果多個邏輯運算符一起使用,必須用括号表明優先級,否則會報錯。

// 報錯
lhs && middle ?? rhs
lhs ?? middle && rhs
lhs || middle ?? rhs
lhs ?? middle || rhs
           

上面四個表達式都會報錯,必須加入表明優先級的括号,如下:

(lhs && middle) ?? rhs;
lhs && (middle ?? rhs);

(lhs ?? middle) && rhs;
lhs ?? (middle && rhs);

(lhs || middle) ?? rhs;
lhs || (middle ?? rhs);

(lhs ?? middle) || rhs;
lhs ?? (middle || rhs);
           

如果對你産生了幫助,請點個贊給我吧!感謝觀看!

繼續閱讀