動態

詳情 返回 返回

ES13的11個超讚的新屬性 - 動態 詳情

聲明:本文為翻譯文章,原文為11 Amazing New JavaScript Features in ES13

像其他語言一樣,JavaScript也在不斷迭代和進化。JS每年都會加入很多新的功能來讓自己變得越發強大,也正是這樣,我們開發者才能寫出更加表意和準確的代碼。

在這篇文章中我們會通過一些例子來看一下最新的ECMAScript 2022(ES13)給我們開發者帶來的11個超讚的新功能。

1. 類成員聲明

在ES13之前,我們只能在構造函數裏面聲明類的成員,而不能像其他大多數語言一樣在類的最外層作用域裏面聲明成員:

class Car {
  constructor() {
    this.color = 'blue';
    this.age = 2;
  }
}

const car = new Car();
console.log(car.color); // blue
console.log(car.age); // 2

老實説這麼寫起來挺不方便的,不過ES13出來之後,這都不算什麼事兒了。現在我們終於可以突破這個限制,寫下面這樣的代碼了:

class Car {
  color = 'blue';
  age = 2;
}

const car = new Car();
console.log(car.color); // blue
console.log(car.age); // 2

2. 給類定義私有方法和成員變量

ES13之前,我們是不可能給類定義私有成員的。所以一些程序員為了表示某個成員變量是一個私有屬性,會給該屬性名添加一個下劃線(_)作為後綴。可是這隻能作為程序員之間的君子之約來使用,因為這些變量其實還是可以被外界訪問到的。

class Person {
  _firstName = 'Joseph';
  _lastName = 'Stevens';

  get name() {
    return `${this._firstName} ${this._lastName}`;
  }
}

const person = new Person();
console.log(person.name); // Joseph Stevens

// 這些所謂的私有屬性其實還是可以被外界訪問到的
console.log(person._firstName); // Joseph
console.log(person._lastName); // Stevens

// 而且還能被改來改去
person._firstName = 'Robert';
person._lastName = 'Becker';

console.log(person.name); // Robert Becker

不過ES13出來後,媽媽再也不用怕我們的私有屬性會被別人訪問和更改了。在ES13中,我們只需要給我們的屬性名添加一個hashtag(#)前綴,這個屬性就變成私有的了。當我們的屬性變為私有後,任何外界對其的訪問都會出錯哦。

class Person {
  #firstName = 'Joseph';
  #lastName = 'Stevens';

  get name() {
    return `${this.#firstName} ${this.#lastName}`;
  }
}

const person = new Person();
console.log(person.name);

// SyntaxError: Private field '#firstName' must be
// declared in an enclosing class
console.log(person.#firstName);
console.log(person.#lastName);

這裏值得一提的是,上面説的SyntaxError是一個編譯時拋出的錯誤,所以你不會等你的代碼運行後才知道這個屬性被非法訪問了。

3. 支持在最外層寫await

我們都知道在JS中,await操作符的作用就是當我們碰到一個promise的時候,我們可以使用await來暫停當前代碼的執行,等到這個promise被settled(fulfilled或者rejected)了,我們才繼續當前代碼的執行。

可是之前使用await的時候有個很頭疼的地方就是一定要在一個async的函數裏面使用而不能在全局作用域裏面使用,像下面這麼寫就會報錯:

function setTimeoutAsync(timeout) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, timeout);
  });
}

// SyntaxError: await is only valid in async functions
await setTimeoutAsync(3000);

ES13出來後,就舒服多了,我們終於可以這麼寫代碼了:

function setTimeoutAsync(timeout) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, timeout);
  })
}

// 慢慢地等時間流逝吧
await setTimeoutAsync(3000);

4. 類支持定義靜態成員和靜態私有方法

在ES13中,我們還可以給類定義靜態成員和靜態私有函數。類的靜態方法可以使用this關鍵字訪問其他的私有或者公有靜態成員,而對於類的實例方法則可以通過this.constructor來訪問這些靜態屬性.

class Person {
  static #count = 0;

  static getCount() {
    return this.#count;
  }

  constructor() {
    this.constructor.#incrementCount();
  }

  static #incrementCount() {
    this.#count++;
  }
}

const person1 = new Person();
const person2 = new Person();

console.log(Person.getCount()); // 2

5. 類支持定義靜態代碼塊

ES13允許在類中通過static關鍵字定義一系列靜態代碼塊,這些代碼塊只會在類被創造的時候執行一次。這其實有點像一些其他的如C#和Java等面向對象的編程語言的靜態構造函數的用法。

一個類可以定義任意多的靜態代碼塊,這些代碼塊會和穿插在它們之間的靜態成員變量一起按照定義的順序在類初始化的時候執行一次。我們還可以使用super關鍵字來訪問父類的屬性。

class Vehicle {
  static defaultColor = 'blue';
}

class Car extends Vehicle {
  static colors = [];

  static {
    this.colors.push(super.defaultColor, 'red');
  }

  static {
    this.colors.push('green');
  }

  console.log(Car.colors); ['blue', 'red', 'green']
}

6. 使用in來判斷某個對象是否擁有某個私有屬性

這個新屬性的名字其實叫做Ergonomic Brand Checks for Private Fields,原諒我才疏學淺,我實在不知道怎麼翻譯,所以大概將它的作用表達了出來。總的來説,它就是用來判斷某個對象是否擁有某個特定的私有屬性,是通過in操作符來判斷的。

class Car {
  #color;

  hasColor() {
    return #color in this;
  }
}

const car = new Car();
console.log(car.hasColor()); // true

這個in操作符甚至還可以區分不同類的同名私有屬性:

class Car {
  #color;

  hasColor() {
    return #color in this;
  }
}

class House {
  #color;

  hasColor() {
    return #color in this;
  }
}

const car = new Car();
const house = new House();

console.log(car.hasColor()); // true
console.log(car.hasColor.call(house)); // false
console.log(house.hasColor()); // true
console.log(house.hasColor.call(car)); // false

7. 使用at函數來索引元素

一般來説如果我們想要訪問數組的第N個元素,我們會使用方括號[N - 1]:

const arr = ['a', 'b', 'c', 'd'];
console.log(arr[1]); //b

這樣寫大多數時候都是沒有什麼問題的,其他語言也是這麼做的,可是當我們需要訪問數組倒數第N個元素的時候,我們的代碼就變得有點奇怪了:

const arr = ['a', 'b', 'c', 'd'];

// 倒數第一個元素
console.log(arr[arr.length - 1]); // d

// 倒數第二個元素
console.log(arr[arr.length - 2]); // c

這麼看你的代碼是不是一下子變醜了?不用怕,ES13的at()函數幫你寫出更優雅的代碼!使用新的new()方法,當我們想要訪問倒數第N個元素時,我們只需要傳入-Nat()即可:

const arr = ['a', 'b', 'c', 'd'];

// 倒數第一個元素
console.log(arr.at(-1)); // d
// 倒數第二個元素
console.log(arr.at(-2)); // c

你看,你的代碼是不是一下子表意多了!除了數組,stringTypedArray對象也支持at()函數哦!

const str = 'Coding Beauty';
console.log(str.at(-1)); // y
console.log(str.at(-2)); // t

const typedArray = new Uint8Array([16, 32, 48, 64]);
console.log(typedArray.at(-1)); // 64
console.log(typedArray.at(-2)); // 48

8. 正則表達式匹配字符串的時候支持返回開始和結束索引

簡單來説這個新屬性就是允許我們告訴RegExp在返回match對象的時候,給我們返回匹配到的子字符串的開始和結束索引。

ES13之前,我們只能獲取正則表達式匹配到的子字符串的開始索引:

const str = 'sun and moon';
const regex = /and/;
const matchObj = regex.exec(str);

// [ 'and', index: 4, input: 'sun and moon', groups: undefined ]
console.log(matchObj);

ES13後,我們就可以給正則表達式添加一個d的標記來讓它在匹配的時候給我們既返回匹配到的子字符串的起始位置還返回其結束位置:

const str = 'sun and moon';
const regex = /and/d;
const matchObj = regex.exec(str);
/**
[
  'and',
  index: 4,
  input: 'sun and moon',
  groups: undefined,
  indices: [ [ 4, 7 ], groups: undefined ]
]
 */
console.log(matchObj);

你看,設置完d標記後,多了一個indices的數組,裏面就是匹配到的子字符串的範圍了!

9. Object.hasOwn()方法

在JS中,我們可以使用Object.prototype.hasOwnProperty()來檢查某個對象自身是否擁有某個屬性:

class Car {
  color = 'green';
  age = 2;
}

const car = new Car();

console.log(car.hasOwnProperty('age')); // true
console.log(car.hasOwnProperty('name')); // false

上面的寫法其實是有兩個問題的。第一個問題是:Object.prototype.hasOwnProperty()這個方法是不受保護的,換句話來説就是它可以被某個類自定義的hasOwnProperty()方法覆蓋掉,而自定義方法做的事情可能和Object.prototype.hasOwnProperty()做的事情完全不一樣:

class Car {
  color = 'green';
  age = 2;
  
  // 你看這個方法就沒有告訴我們這個類的對象是不是有某個屬性
  hasOwnProperty() {
    return false;
  }
}

const car = new Car();

console.log(car.hasOwnProperty('age')); // false
console.log(car.hasOwnProperty('name')); // false

上面的寫法第二個問題就是:當一個對象是通過Object.create(null)創建出來的具有null原型的對象時,你想在這個對象上面調用hasOwnProperty這個方法是會報錯的:

const obj = Object.create(null);
obj.color = 'green';
obj.age = 2;

// TypeError: obj.hasOwnProperty is not a function
console.log(obj.hasOwnProperty('color'));

解決這個問題的一種辦法就是調用Object.prototype.hasOwnProperty這個Functioncall方法:

const obj = Object.create(null);
obj.color = 'green';
obj.age = 2;
obj.hasOwnProperty = () => false;

Object.prototype.hasOwnProperty.call(obj, 'color')

hasOwnProperty需要被多次調用的時候,我們可以通過將這部分邏輯抽象成一個方法來減少重複的代碼:

function objHasOwnProp(obj, propertyKey) {
  return Object.prototype.hasOwnProperty.call(obj, propertyKey);
}

const obj = Object.create(null);
obj.color = 'green';
obj.age = 2;
obj.hasOwnProperty = () => false;

console.log(objHasOwnProp(obj, 'color')); // true
console.log(objHasOwnProp(obj, 'name')); // false

封裝是封裝了,不過看着好麻煩有木有?所以ES13誕生了一個全新的Object.hasOwn()函數來幫我們做上面這些重複的工作。這個新的內置函數接收兩個參數,一個是對象,一個是屬性,如果這個對象本身就有這個屬性的話,這個函數就會返回true,否則就返回false

const obj = Object.create(null);
obj.color = 'green';
obj.age = 2;
obj.hasOwnProperty = () => false;

console.log(Object.hasOwn(obj, 'color')); // true
console.log(Object.hasOwn(obj, 'name')); // false

10. Error對象的Cause屬性

ES13後,Error對象多了一個cause屬性來指明錯誤出現的原因。這個屬性可以幫助我們為錯誤添加更多的上下文信息,從而幫助使用者們更好地定位錯誤。這個屬性是我們在創建error對象時傳進去的第二個參數對象的cause屬性:

function userAction() {
  try {
    apiCallThatCanThrow();
  } catch (err) {
    throw new Error('New error message', { cause: err });
  }
}

try {
  userAction();
} catch (err) {
  console.log(err);
  console.log(`Cause by: ${err.cause}`);
}

11. 數組支持逆序查找

在JS中,我們可以使用數組的find()函數來在數組中找到第一個滿足某個條件的元素。同樣地,我們還可以通過findIndex()函數來返回這個元素的位置。可是,無論是find()還是findIndex(),它們都是從數組的頭部開始查找元素的,可是在某些情況下,我們可能有從數組後面開始查找某個元素的需要。例如我們知道待查找的元素在比較靠後的位置,從後面開始尋找的話會有更好的性能,就像下面這個例子:

const letters = [
  { value: 'v' },
  { value: 'w' },
  { value: 'x' },
  { value: 'y' },
  { value: 'z' },
];

// 我們想要找的y元素比較靠後, 順序查找性能不好
const found = letters.find((item) => item.value === 'y');
const foundIndex = letters.findIndex((item) => item.value === 'y');

console.log(found); // { value: 'y' }
console.log(foundIndex); // 3

在這種情況下使用find()findIndex()也可以,就是性能差點而已。ES13出來後,我們終於有辦法處理這種情況了,那就是使用新的findLast()findLastIndex()函數。這兩個函數都會從數組的末端開始尋找某個滿足條件的元素:

const letters = [
  { value: 'v' },
  { value: 'w' },
  { value: 'x' },
  { value: 'y' },
  { value: 'z' },
];

// 後序查找一下子快了,有木有
const found = letters.findLast((item) => item.value === 'y');
const foundIndex = letters.findLastIndex((item) => item.value === 'y');

console.log(found); // { value: 'y' }
console.log(foundIndex); // 3

另外一種使用findLast()findLastIndex()的場景就是我們本身就是想要尋找最後一個滿足某個條件的元素,例如找到數組裏面最後一個偶數,這個時候還用find()findIndex()的話得到的結果是錯誤的:

const nums = [7, 14, 3, 8, 10, 9];

// 返回了14, 結果應該是10才對
const lastEven = nums.find((value) => value % 2 === 0);

// 返回了1, 結果應該是4才對
const lastEvenIndex = nums.findIndex((value) => value % 2 === 0);
console.log(lastEven); // 14
console.log(lastEvenIndex); // 1

試想一下要得到正確的答案,我們還要使用find()findIndex()的話,要怎麼改呢?首先我們需要用reverse()來反轉數組,然後再調用find()findIndex()函數。不過這個做法有兩個問題,一個問題就是需要改變原數組,這個問題可以通過拷貝數組來解決,佔用空間多點而已。另一個問題是findIndex()得到索引後我們還要做一些額外的計算才能得到元素原數組的位置,具體做法是:

const nums = [7, 14, 3, 8, 10, 9];

// 在調用reverse之前先拷貝函數,以防改變原數組
const reversed = [...nums].reverse();
// 這次返回對了,是10
const lastEven = reversed.find((value) => value % 2 === 0);
// findIndex得到的結果還是不對的
const reversedIndex = reversed.findIndex((value) => value % 2 === 0);
// 需要多一步計算才能得出正確結果
const lastEvenIndex = reversed.length - 1 - reversedIndex;
console.log(lastEven); // 10
console.log(reversedIndex); // 1
console.log(lastEvenIndex); // 4

看着的確麻煩,findLast()findLastIndex()出來後,數組就支持後序查找元素了,實現同樣的需求,代碼一下子簡潔了不少:

const nums = [7, 14, 3, 8, 10, 9];

const lastEven = nums.findLast((num) => num % 2 === 0);
const lastEvenIndex = nums.findLastIndex((num) => num % 2 === 0);

console.log(lastEven); // 10
console.log(lastEvenIndex); // 4

你看代碼是不是短了很多,並且可讀性和正確性都提高了!

結論

上面我們介紹了ES13最新的11個屬性。作為一個開發者,我們可以使用它們來提高自己的生產效率和編寫更加簡潔和表意的代碼,你還不趕緊在項目裏面實踐一把?

個人技術動態

關注我的公眾號 - 進擊的大葱獲取我的最新技術推送

wechat.jpeg

Add a new 評論

Some HTML is okay.