Kurt/JS ES13 最新的特性

Created Wed, 20 Jul 2022 16:11:13 +0800 Modified Wed, 02 Nov 2022 05:33:26 +0000
2533 Words 12 min

2022 JavaScript Newest Features (ES13  新特性)

ES13 最新的 Features:

  • Class field declaration
  • Ergonomic brand checks for private fields
  • Class Static Block
  • Regexp Match Indices
  • Top-level await
  • .at() function for Indexing
  • Accessible Object.prototype.hasOwnProperty()
  • Error Cause
  • Array find from last

以上新特性在大多瀏覽器都已實作,但少部份不支援,可以在 Can I Use 上查詢支援的情況。

Class field declarations

在 ES2015 時定義一個 class 會像下面這樣,fields 會被寫在 constructor 裡面

class Person {
  constructor() {
    this.name = "Kurt";
  }

  getName() {
    return this.name;
  }
}

ES13 則可以寫成

class Person {
  name = "Kurt";

  getName() {
    return this.name;
  }
}

除此之外,以前我們可能會使用底線當作前綴來定義 private fields,但其實那只能當作一種命名規範,卻沒有實質作用

class Person {
  constructor() {
    this.name = "Kurt";
    this._age = 25;
  }
}
let p = new Person();
console.log(p.name); // Kurt
console.log(p._age); // 25

在 ES13 可以使用 # 來表示 private fields,如果試著要存取會出現錯誤

class Person {
  name = "Kurt";
  #age = 25;
}
let p = new Person();
console.log(p.#age); // throw an error

除了一般的 fields 外,methods 一樣也可以設為 private

class Person {
  name = "Kurt";
  #age = 25;
  test() {
    console.log("Hi, ");
    this.#ttt();
  }
  #ttt() {
    console.log(this.name);
  }
}
let p = new Person();
p.test(); // Hi, Kurt
p.#ttt(); // throw an Error

Ergonomic brand checks for private fields

有了第一個提到的 private 特性,我們可能有時需要知道該類別是否含有某個 field,但因為在外部存取帶有 # 的欄位會導致 Excpetion,如果每次都要寫 try/catch 檢查好像又不那麼方便,因此這個語法就誕生了  -  in

class A {
  #a;

  static isA(obj) {
    return #a in obj;
  }
}
class B {
  #b;
}
let a = new A();
let b = new B();
A.isA(a); // true
A.isA(b); // false

Class Static Block

static blocks 就像 static fields 一樣,是在 class 剛建立時就執行,使 class 建立時可以執行一些初始化的動作,此特性也存在其他程式語言裡,如 Java。 例如 class 中的 y, z fields 會因為不同的情況而給予不同的值,若沒有 static block,則必須把邏輯寫在 class 外部,這可能會造成讀程式的人沒辦法一眼就看出此段邏輯是屬於該 class 的

// 沒有 static block 的寫法
class C {
 static x = ;
 static y;
 static z;
}
try {
  const obj = doSomethingWith(C.x);
  C.y = obj.y
  C.z = obj.z;
}
catch {
  C.y = ;
  C.z = ;
}


// 加上 static block 寫法
class C {
  static x = ...;
  static y;
  static z;
  static {
    try {
      const obj = doSomethingWith(this.x);
      this.y = obj.y;
      this.z = obj.z;
    }
    catch {
      this.y = ...;
      this.z = ...;
    }
  }
}

另一個例子是假如我們的 class C 要和另一個 class 或 function 共享一個 private field

let getX;
class C {
  #x;
  constructor(x) {
    this.#x = x;
  }

  static {
    getX = (obj) => obj.#x;
  }
}
getX(new C("123")); // 123

RegExp Match Indices

當我們使用 exec 取得正則表達的結果時,最多只能知道匹配的開始位置,但是無法知道結束位置,此特性就是為了解決這件事,只要在你的正則表達式最後加上 d,即可在 exec 的結果中得到新的欄位 indices,其代表匹配的起訖位置

  • 沒有加上 d 得到的結果

const str = "bbabcddd";
const regex = /(abc)/g;

const matched = regex.exec(str);

console.log(matched);
//[
//  "abc",
//  "abc",
//  groups: undefined,
//  index: 2,
//  input: "bbabcddd"
//]
  • 加上 d 額外得到 indices 資訊

const str = "bbabcddd";
const regex = /(abc)/dg;

const matched = regex.exec(str);

console.log(matched);
// [
//   "abc",
//   "abc",
//   groups: undefined,
//   index: 2,
//   indices: [
//     [2, 5],
//     [2, 5],
//     groups: undefined
//   ],
//   input: "bbabcddd"
// ]

特別提一下,為了效能考量,只有在加入 d 的時候才會額外回傳 indices 資訊

Top-level await

在 ES13 之前, await 只能用在 async 裡面,但現在可以在外部使用了(僅限於 JavaScript modules),主要是把 JS modules 當作一個很大的 async function 來達成這件事! 此提案的動機請看下列程式碼 ⬇️

// awaiting.mjs
import { process } from "./some-module.mjs";
let output;
async function main() {
  const dynamic = await import(computedModuleSpecifier);
  const data = await fetch(url);
  output = process(dynamic.default, data);
}
main();
export { output };
// usage.mjs
import { output } from "./awaiting.mjs";
export function outputPlusValue(value) { return output + value; }

console.log(outputPlusValue(100));
setTimeout(() => console.log(outputPlusValue(100), 1000);

你可以看到 usage.mjs 從 awaiting.mjs 引入了 output,看似沒問題,但其實這樣的寫法會導致 output 的結果是 undefined,因為 awaiting.mjs 中的 output 是非同步的,其解法可以改寫為以下 ⬇️

// awaiting.mjs
import { process } from "./some-module.mjs";
let output;
export default (async () => {
  const dynamic = await import(computedModuleSpecifier);
  const data = await fetch(url);
  output = process(dynamic.default, data);
})();
export { output };
// usage.mjs
import promise, { output } from "./awaiting.mjs";
export function outputPlusValue(value) { return output + value }

promise.then(() => {
  console.log(outputPlusValue(100));
  setTimeout(() => console.log(outputPlusValue(100), 1000);
});

以上程式碼可以看到 awaiting.mjs 一併把 promise 給導出來,然後在 usage.mjs 就蠆 promise.then…執行完後得到 output。

雖然看似解決了目前的問題,但這樣的寫法成本太高

  1. 你必須確保協作者都完全理解 Promise 的運作機制
  2. 不能忘記使用這樣的寫法,不然就有可能導致錯誤
  3. 在多層的模組下,Promise 必須準確的一步一步的執行

Top-level await 解決了以上問題,上面的程式碼使用 top-level await 的寫法可參考下方 ⬇️

// awaiting.mjs
import { process } from "./some-module.mjs";
const dynamic = import(computedModuleSpecifier);
const data = fetch(url);
export const output = process((await dynamic).default, await data);
// usage.mjs
import { output } from "./awaiting.mjs";
export function outputPlusValue(value) {
  return output + value;
}

console.log(outputPlusValue(100));
setTimeout(() => console.log(outputPlusValue(100), 1000));

使用 top-level await,非同步取得資源時,不用再把 await 包在 async 裡面了,且在 usage.mjs 引入時也不用再關心該模組是否有使用到非同步機制。

以下三種情況你可能會使用到該特性:

  1. Dynamic dependency pathing
const strings = await import(`/i18n/${navigator.language}`); 2. Resource initialization
  1. Resource initialization
const connection = await dbConnector();
  1. Dependency fallbacks
let jQuery;
try {
  jQuery = await import("https://cdn-a.com/jQuery");
} catch {
  jQuery = await import("https://cdn-b.com/jQuery");
}

.at() function for Indexing

目前我們要取得 Array 的最後一個元素,必須要寫成 arr[arr.length-1] ,不像 python 可以寫 arr[-1] ,但在此提案中有提到 arr[-1] 這種寫法在 JS 中是不可能的,因為 [] 不僅只用於 Array,而是所有 Object 都能使用,所以當你寫 arr[-1] ,實際上會得到的是 -1 這個 property 的值。 但在 ES13 多了 at() 讓我們達成這件事,範例請看下方 ⬇️

// not use at()

const arr = [1, 2, 3];
arr[0]; // 1
arr[arr.length - 2]; // 2
arr.slice(-2)[0]; // 2

// use at()

const arr = [1, 2, 3];
arr.at(0); // 1
arr.at(-2); // 2

const str = "ABC";
str.at(-1); // 'C'
str.at(0); // 'A'

Accessible Object.prototype.hasOwnProperty()

這個提案不算是新功能,而是把原本的 Object.hasOwnProperty,變得比較容易寫,只需要寫 hasOwn 即可,相關的簡化在某些 node packages 也有實作,像是 has、lodash.has

const obj = { data: "123" };
Object.hasOwn(object, "data"); // true

const person = Object.create({ name: "Kurt" });
Object.hasOwn(person, "name"); // false
Object.hasOwn(person.__proto__, "name"); // true

const obj2 = Object.create(null);
Object.hasOwn(obj2, "name"); // false

除此之外,特別提一下在 MDN 有建議我們不要在 Object 的原型之外使用 hasOwnProperty,因為 JS 不能保證此特性的存在,例如:使用 Object.create(null) 產生的物件就不會繼承 Object.prototype 中的特性,所以 hasOwnProperty 在這時候使用就會出錯。

Error Cause

如果 Error 是從很深層發出的,那我們可能會花很多時間去一層一層看是哪邊出錯,所以通常我們都會包裝自己的 CustomError 類別,然後加上 reason 欄位之類的,把錯誤訊息寫的更明確,而在 ES13 可以使用 cause,讓多個 erros 串聯在一起。

async function doJob() {
  const rawResource = await fetch("//domain/resource-a").catch((err) => {
    throw new Error("Download raw resource failed", { cause: err });
  });
  const jobResult = doComputationalHeavyJob(rawResource);
  await fetch("//domain/upload", { method: "POST", body: jobResult }).catch(
    (err) => {
      throw new Error("Upload job result failed", { cause: err });
    }
  );
}

try {
  await doJob();
} catch (e) {
  console.log(e);
  console.log("Caused by", e.cause);
}
// Error: Upload job result failed
// Caused by TypeError: Failed to fetch

Array find from last

Array 雖然已經有 find、findIndex 可以使用,但是這兩個方法都是從頭開始遍歷,如果我們想從最後遍歷到第 1 個,第 1 個直覺是使用 reverse,但是這樣會造成原始的 Array 被改變,如果要不改變原始的 Array,又必須要多餘的複製 Array,在 ES13 可以使用 findLast、findLastIndex 來達到此目的。

1. 使用 reverse 反轉陣列後再使用 find、findIndex

const array = [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }];

array.find((n) => n.value % 2 === 1); // { value: 1 }
array.findIndex((n) => n.value % 2 === 1); // 0

// find
[...array].reverse().find((n) => n.value % 2 === 1); // { value: 3 }

// findIndex
array.length - 1 - [...array].reverse().findIndex((n) => n.value % 2 === 1); // 2
array.length - 1 - [...array].reverse().findIndex((n) => n.value === 42); // should be -1, but 4

2. 使用 findLast、findLastIndex

const array = [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }];

array.findLast((n) => n.value % 2 === 1); // { value: 3 }

array.findLastIndex((n) => n.value % 2 === 1); // 2
array.findLastIndex((n) => n.value === 42); // -1

此特性算是最新的,在支援度的部份還不是那麼高,除了 IE 不說,Firefox 目前也還沒支援,所以不太建議使用。


Reference: