ECMAScript 12 學習筆記
Logical Assignment Operators
ES2021 引入了三個新的邏輯賦值運算子
||=
、&&=
、??=
,將邏輯運算子與賦值運算子進行結合。
// Logical OR assignment (||=)
x ||= y
// 等同於
x = x || y
// Logical AND assignment (&&=)
x &&= y
// 等同於
x = x && y
// Nullish coalescing assignment (??=)
x ??= y
// 等同於
x = x ?? y
它們的一個用途是,為變數或屬性設定預設值。
// 舊的寫法
user.id = user.id || 1;
// 新的寫法
user.id ||= 1;
user.id
屬性如果不存在,則設為1
// 舊的寫法
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
這個新特性是為了方便查看程式碼而出現的,如果今天有一個數值比較大,那麼看起來就不是那麼一目了然。例如:
const num = 123456789;
ES2021,允許 JavaScript 的數值使用下底線(_
)作為分隔符。
// 數值分隔符沒有指定間隔的位數
const num = 123_456_78_9;
const num2 = 123456789;
console.log(num === num2); // true
除了十進位,其他進位的數值也可以使用分隔符。
const binary = 0b0010_1010;
const octal = 0o12_34_56;
const hex = 0xa1_b3_c3;
數值分隔符有幾個使用注意點:
- 不能放在數值的最前面(leading)或最後面(trailing)。
- 不能兩個或兩個以上的分隔符連在一起。
- 小數點的前後不能有分隔符。
- 科學記號裡面,表示指數的
e
或E
前後不能有分隔符。
// 全部報錯
3_.141
3._141
1_e12
1e_12
123__456
_1464301
1464301_
replaceAll()
所有比對都會被替代項替換。模式可以是字串或是正規表達式,而替換項可以是字串或針對每次比對執行的函式,並回傳一個全新的字串。
在之前字串的實體方法 replace()
只能替換一個比對。
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
來處理。
console.log(str.replace(/wish/g, '*'));
但是使用正規表達式畢竟不是那麼方便和直觀,ES2021 引入了 replaceAll()
方法,可以一次性替換所有比對。
// String.prototype.replaceAll(searchValue, replacement)
const newStr = str.replaceAll('wish', '*');
console.log(newStr);
Promise.any()
ES2021 引入了 Promise.any()
方法。該方法接受一組 Promise 實體作為參數,包裝成一個新的 Promise 實體回傳。 只要參數實體有一個變成 fulfilled
狀態,包裝實體就會變成 fulfilled
狀態;如果所有參數實體都變成 rejected
狀態,包裝實體就變成 rejected
狀態。
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
使用的例子:
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 操作。其中只要有一個變成 fulfilled
,Promise.any()
回傳的 Promise 物件就變成 fulfilled
。如果所有三個操作都變成 rejected
,那麼 await
指令就會拋出錯誤,而這個拋出的錯誤是一個 AggregateError 實體。
為了配合新增的 Promise.any()
方法,ES2021 還引入一個新的錯誤物件 AggregateError,如果某個單一操作,同時引發了多個錯誤,需要同時拋出這些錯誤,那麼就可以拋出一個AggregateError 錯誤物件,把各種錯誤都放在這個物件裡面。
WeakRef
在一般情況下,物件的引用是強引用的,這意味著只要持有物件的引用,它就不會被垃圾回收(GC)。只有當該物件沒有任何的強引用時,垃圾回收機制才會銷毀該物件並且回收該物件所佔的記憶體空間。
而
weakRef
允許你保留對另一個物件的弱引用,而不會阻止該弱引用物件被垃圾回收。
回顧 ES6 WeakSet & WeakMap
- WeakSet
- 成員只能是複合資料型態
- 不存在引用計數 +1
- size, for 不能用了
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
- key 只能是複合資料型態
- key 為弱引用,隨時有可能被 GC
- 同樣無法遍歷
WeakMap 只接受物件作為 key(null
除外),不接受其他類型的值作為 key。
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 也可以一起被清掉。
於是第一反應可能會這樣去寫:
<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 給清除。
let like = document.getElementById('like');
// 略
setTimeout(() => {
document.body.removeChild(like);
like = null
}, 2000);
ES12 的 WeakRef
WeakSet 和 WeakMap 是基於弱引用的資料結構,ES2021 更進一步,提供了 WeakRef 物件,用於直接建立物件的弱引用。
let obj = {
name: 'sheep',
};
let wobj = new WeakRef(obj);
WeakRef 實體物件有一個 deref()
方法,如果原始物件存在,該方法回傳原始物件;如果原始物件已經被垃圾回收機制清除,該方法回傳 undefined
。
現在來改寫一下剛才的情境案例:
<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。
let obj = {
name: 'sheep',
};
const registry = new FinalizationRegistry((data) => {
console.log('銷毀了', data);
});
registry.register(obj, '1111111');
再把剛剛的情景多加一個功能就是在按鈕消失時回傳一共點了幾下。
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);