Skip to content

ECMAScript 12 學習筆記

參考學習資源:阮一峰 ECMAScript 6 (ES6) 標準入門教程

Logical Assignment Operators

ES2021 引入了三個新的邏輯賦值運算子 ||=&&=??=,將邏輯運算子與賦值運算子進行結合。

js
// Logical OR assignment (||=)
x ||= y
// 等同於
x = x || y

// Logical AND assignment (&&=)
x &&= y
// 等同於
x = x && y

// Nullish coalescing assignment (??=)
x ??= y
// 等同於
x = x ?? y

它們的一個用途是,為變數或屬性設定預設值。

js
// 舊的寫法
user.id = user.id || 1;

// 新的寫法
user.id ||= 1;

user.id 屬性如果不存在,則設為 1

js
// 舊的寫法
function example(opts) {
  opts.foo = opts.foo ?? 'bar';
  opts.baz ?? (opts.baz = 'qux');
}

// 新的寫法
function example(opts) {
  opts.foo ??= 'bar';
  opts.baz ??= 'qux';
}

參數物件 opts 如果不存在屬性 foo 和屬性 baz,則為這兩個屬性設定預設值。

Numeric Separators

這個新特性是為了方便查看程式碼而出現的,如果今天有一個數值比較大,那麼看起來就不是那麼一目了然。例如:

js
const num = 123456789;

ES2021,允許 JavaScript 的數值使用下底線(_)作為分隔符。

js
// 數值分隔符沒有指定間隔的位數
const num = 123_456_78_9;
const num2 = 123456789;

console.log(num === num2); // true

除了十進位,其他進位的數值也可以使用分隔符。

js
const binary = 0b0010_1010;
const octal = 0o12_34_56;
const hex = 0xa1_b3_c3;

數值分隔符有幾個使用注意點:

  • 不能放在數值的最前面(leading)或最後面(trailing)。
  • 不能兩個或兩個以上的分隔符連在一起。
  • 小數點的前後不能有分隔符。
  • 科學記號裡面,表示指數的eE前後不能有分隔符。
js
// 全部報錯
3_.141
3._141
1_e12
1e_12
123__456
_1464301
1464301_

replaceAll()

所有比對都會被替代項替換。模式可以是字串或是正規表達式,而替換項可以是字串或針對每次比對執行的函式,並回傳一個全新的字串。

在之前字串的實體方法 replace() 只能替換一個比對。

js
const str = `I wish to wish the wish you wish to wish, 
but if you wish the wish the witch wishes, 
I won't wish the wish you wish to wish.`;

console.log(str.replace('wish', '*')); // replace 只能將第一個 wish 替換成 *

如果要將所有的 wish 都給換成星號,就不得不使用正規表達式的 g 來處理。

js
console.log(str.replace(/wish/g, '*'));

但是使用正規表達式畢竟不是那麼方便和直觀,ES2021 引入了 replaceAll() 方法,可以一次性替換所有比對。

js
// String.prototype.replaceAll(searchValue, replacement)
const newStr = str.replaceAll('wish', '*');
console.log(newStr);

Promise.any()

ES2021 引入了 Promise.any() 方法。該方法接受一組 Promise 實體作為參數,包裝成一個新的 Promise 實體回傳。 只要參數實體有一個變成 fulfilled 狀態,包裝實體就會變成 fulfilled 狀態;如果所有參數實體都變成 rejected 狀態,包裝實體就變成 rejected 狀態。

js
Promise.any([
  fetch('https://v8.dev/').then(() => 'home'),
  fetch('https://v8.dev/blog').then(() => 'blog'),
  fetch('https://v8.dev/docs').then(() => 'docs')
]).then((first) => {  // 只要有一個 fetch() 請求成功
  console.log(first);
}).catch((error) => { // 所有三個 fetch() 全部請求失敗
  console.log(error);
});

Promise.any()Promise.race() 很像,只有一個點不同,就是 Promise.any() 不會因為某個 Promise 變成 rejected 狀態而結束,必須等到所有參數 Promise 變成 rejected 狀態才會結束。

下面是一個結合 await 使用的例子:

js
const promises = [ajax1(), ajax2(), ajax3()];

async function request() {
  try {
    const first = await Promise.any(promises);
    console.log(first);
  } catch (error) {
    console.log(error); // AggregateError: All promises were rejected
  }
}
request();

參數陣列包含三個 Promise 操作。其中只要有一個變成 fulfilledPromise.any() 回傳的 Promise 物件就變成 fulfilled。如果所有三個操作都變成 rejected,那麼 await指令就會拋出錯誤,而這個拋出的錯誤是一個 AggregateError 實體。

為了配合新增的 Promise.any()方法,ES2021 還引入一個新的錯誤物件 AggregateError,如果某個單一操作,同時引發了多個錯誤,需要同時拋出這些錯誤,那麼就可以拋出一個AggregateError 錯誤物件,把各種錯誤都放在這個物件裡面。

WeakRef

在一般情況下,物件的引用是強引用的,這意味著只要持有物件的引用,它就不會被垃圾回收(GC)。只有當該物件沒有任何的強引用時,垃圾回收機制才會銷毀該物件並且回收該物件所佔的記憶體空間。

weakRef 允許你保留對另一個物件的弱引用,而不會阻止該弱引用物件被垃圾回收。

回顧 ES6 WeakSet & WeakMap

  • WeakSet
  1. 成員只能是複合資料型態
  2. 不存在引用計數 +1
  3. size, for 不能用了
js
let obj = {
  name: 'sheep',
};

const s1 = new WeakSet();
s1.add(obj);
// s1.add('aaaaa'); // WeakSet 的成員只能是物件,而不能是其他型別的值。

obj = null;

如果今天 obj 因為一些情境被重新賦值成了 null,就會觸發 GC,此時去控制台查看就會發現 s1 裡已經變成空了,因為弱引用不會被算入引用計數。

另外,由於 WeakSet 內部有多少個成員,取決於垃圾回收機制有沒有執行,執行前後很可能成員個數是不一樣的,而垃圾回收機制何時執行是不可預測的,因此 ES6 規定 WeakSet 不可遍歷。

  • WeakMap
  1. key 只能是複合資料型態
  2. key 為弱引用,隨時有可能被 GC
  3. 同樣無法遍歷

WeakMap 只接受物件作為 key(null除外),不接受其他類型的值作為 key。

js
const map = new WeakMap();
map.set(1, 2)
// TypeError: 1 is not an object!
map.set(Symbol(), 2)
// TypeError: Invalid value used as weak map key
map.set(null, 2)
// TypeError: Invalid value used as weak map key

不過,現在有一個提案,允許 Symbol 值也可以作為 WeakMap 的 key。一旦納入標準,就意味著鍵名存在兩種可能:物件和 Symbol 值。

實際使用情境:

假設有一個按鈕,使用者點擊它可以計數,但如果這個節點之後因為某些因素被移除了,就可以使用 WeakMap 來弱引用這個節點,讓它的 key 也可以一起被清掉。

於是第一反應可能會這樣去寫:

html
<body>
  <button id="like">點讚</button>
  <script>
    const wmap = new WeakMap();
    const like = document.getElementById('like');

    wmap.set(like, { click: 0 });

    like.onclick = function () {
      let times = wmap.get(like);
      times.click++;
    };

    setTimeout(() => {
      document.body.removeChild(like);
    }, 2000);
  </script>
</body>

然後在控制台查看後會發現到 button 節點的 key 還在只是找不到這個節點

這是因為這個 like 變數是一個強引用,上方程式碼並沒有手動去將 like 給清除。

js
let like = document.getElementById('like');

// 略

setTimeout(() => {
  document.body.removeChild(like);
  like = null
}, 2000);

ES12 的 WeakRef

WeakSet 和 WeakMap 是基於弱引用的資料結構,ES2021 更進一步,提供了 WeakRef 物件,用於直接建立物件的弱引用。

js
let obj = {
  name: 'sheep',
};

let wobj = new WeakRef(obj);

WeakRef 實體物件有一個 deref() 方法,如果原始物件存在,該方法回傳原始物件;如果原始物件已經被垃圾回收機制清除,該方法回傳 undefined

現在來改寫一下剛才的情境案例:

html
<body>
  <button id="like">like</button>
  <script>
    const wmap = new WeakMap();
    const like = new WeakRef(document.getElementById('like'));

    wmap.set(like.deref(), { click: 0 });

    like.deref().onclick = function () {
      let times = wmap.get(like.deref());
      times.click++;
    };

    setTimeout(() => {
      document.body.removeChild(like.deref());
    }, 2000);
  </script>
</body>

在上面範例中,deref()方法可以判斷原始物件是否已被清除。

FinalizationRegistry

用來指定目標物件被垃圾回收機制清除以後,所要執行的 callback。

js
let obj = {
  name: 'sheep',
};

const registry = new FinalizationRegistry((data) => {
  console.log('銷毀了', data);
});

registry.register(obj, '1111111');

再把剛剛的情景多加一個功能就是在按鈕消失時回傳一共點了幾下。

js
const registry = new FinalizationRegistry((data) => {
  console.log('銷毀了', data);
});
const wmap = new WeakMap();
const like = new WeakRef(document.getElementById('like'));

wmap.set(like.deref(), { click: 0 });

like.deref().onclick = function () {
  let times = wmap.get(like.deref());
  times.click++;
};

setTimeout(() => {
  registry.register(like.deref(), wmap.get(like.deref()).click);
  document.body.removeChild(like.deref());
}, 3000);