ECMAScript 2024(ES15)將帶來這些新特性,超實用!
ECMAScript 語言規范每年都會進行一次更新,而備受期待的 ECMAScript 2024 將于 2024 年 6 月正式亮相。目前,ECMAScript 2024 的候選版本已經發布,為我們帶來了一系列實用的新功能。接下來,就讓我們一起先睹為快吧!
全文概覽:
- Promise.withResolvers
- Object.groupBy / Map.groupBy
- ArrayBuffer.prototype.resize
- ArrayBuffer.prototype.transfer
- String.prototype.isWellFormed
- String.prototype.toWellFormed
- Atomics.waitAsync
- 正則表達式 v 標志
Promise.withResolvers()
Promise.withResolvers() 允許創建一個新的 Promise,并同時獲得 resolve 和 reject 函數。這在某些場景下非常有用,特別是當需要同時訪問到 Promise 的 resolve 和 reject 函數時。
Promise.withResolvers() 完全等同于以下代碼:
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
通常,當創建一個新的 Promise 時,會傳遞一個執行器函數給 Promise 構造函數,這個執行器函數接收兩個參數:resolve 和 reject 函數。但在某些情況下,可能想要在 Promise 創建之后仍然能夠訪問到這兩個函數。這就是 Promise.withResolvers() 的用武之地。
使用 Promise.withResolvers() 的一個例子:
const { promise, resolve, reject } = Promise.withResolvers();
// 在這里可以使用 resolve 和 reject 函數
setTimeout(() => resolve('成功!'), 1000);
promise.then(value => {
console.log(value); // 輸出: 成功!
});
在這個例子中,首先通過 Promise.withResolvers() 創建了一個新的 Promise,并同時獲得了 resolve 和 reject 函數。然后,在 setTimeout 回調中使用 resolve 函數來解析這個 Promise。最后,添加了一個 .then 處理程序來處理 Promise 解析后的值。
使用 Promise.withResolvers() 關鍵的區別在于解決和拒絕函數現在與 Promise 本身處于同一作用域,而不是在執行器中被創建和一次性使用。這可能使得一些更高級的用例成為可能,例如在重復事件中重用它們,特別是在處理流和隊列時。這通常也意味著相比在執行器內包裝大量邏輯,嵌套會更少。
這個功能對于那些需要更細粒度控制 Promise 的狀態,或者在 Promise 創建后仍然需要訪問 resolve 和 reject 函數的場景來說非常有用。
Object.groupBy / Map.groupBy
Object.groupBy 和 Map.groupBy 方法用于數組分組。
假設有一個由表示人員的對象組成的數組,需要按照年齡進行分組。可以使用forEach循環來實現,代碼如下:
const people = [
{ name: "Alice", age: 28 },
{ name: "Bob", age: 30 },
{ name: "Eve", age: 28 },
];
const peopleByAge = {};
people.forEach((person) => {
const age = person.age;
if (!peopleByAge[age]) {
peopleByAge[age] = [];
}
peopleByAge[age].push(person);
});
console.log(peopleByAge);
輸出結果如下:
{
"28": [{"name":"Alice","age":28}, {"name":"Eve","age":28}],
"30": [{"name":"Bob","age":30}]
}
也可以使用reduce方法:
const peopleByAge = people.reduce((acc, person) => {
const age = person.age;
if (!acc[age]) {
acc[age] = [];
}
acc[age].push(person);
return acc;
}, {});
無論哪種方式,代碼都略顯繁瑣。每次都要檢查對象,看分組鍵是否存在,如果不存在,則創建一個空數組,并將項目添加到該數組中。
使用 Object.groupBy 分組
可以通過以下方式來使用新的Object.groupBy方法:
const peopleByAge = Object.groupBy(people, (person) => person.age);
可以看到,代碼非常簡潔!
不過需要注意,使用Object.groupBy方法返回一個沒有原型(即沒有繼承任何屬性和方法)的對象。這意味著該對象不會繼承Object.prototype上的任何屬性或方法,例如hasOwnProperty或toString等。雖然這樣做可以避免意外覆蓋Object.prototype上的屬性,但也意味著不能使用一些與對象相關的方法。
const peopleByAge = Object.groupBy(people, (person) => person.age);
console.log(peopleByAge.hasOwnProperty("28"));
// TypeError: peopleByAge.hasOwnProperty is not a function
在調用Object.groupBy時,傳遞給它的回調函數應該返回一個字符串或 Symbol 類型的值。如果回調函數返回其他類型的值,它將被強制轉換為字符串。
在這個例子中,回調函數返回的是一個數字類型的age屬性值,但由于Object.groupBy方法要求鍵必須是字符串或 Symbol 類型,所以該數字會被強制轉換為字符串類型。
console.log(peopleByAge[28]);
// => [{"name":"Alice","age":28}, {"name":"Eve","age":28}]
console.log(peopleByAge["28"]);
// => [{"name":"Alice","age":28}, {"name":"Eve","age":28}]
使用 Map.groupBy 分組
Map.groupBy和Object.groupBy幾乎做的是相同的事情,只是返回的結果類型不同。Map.groupBy返回一個Map對象,而不是像Object.groupBy返回一個普通的對象。
const ceo = { name: "Jamie", age: 40, reportsTo: null };
const manager = { name: "Alice", age: 28, reportsTo: ceo };
const people = [
ceo
manager,
{ name: "Bob", age: 30, reportsTo: manager },
{ name: "Eve", age: 28, reportsTo: ceo },
];
const peopleByManager = Map.groupBy(people, (person) => person.reportsTo);
這里根據人的匯報上級將他們進行了分組。如果想通過對象來從這個Map中獲取數據,那么要求這些對象具有相同的身份或引用。這是因為Map在比較鍵時使用的是嚴格相等(===),只有兩個對象具有相同的引用,才能被認為是相同的鍵。
peopleByManager.get(ceo);
// => [{ name: "Alice", age: 28, reportsTo: ceo }, { name: "Eve", age: 28, reportsTo: ceo }]
peopleByManager.get({ name: "Jamie", age: 40, reportsTo: null });
// => undefined
在上面的例子中,如果嘗試使用與ceo對象類似的對象作為鍵去訪問Map中的項,由于這個對象與之前存儲在Map中的ceo對象不是同一個對象,所以無法檢索到對應的值。
ArrayBuffer.prototype.resize
ArrayBuffer 實例的 resize() 方法將 ArrayBuffer 調整為指定的大小,以字節為單位,前提是該 ArrayBuffer 是可調整大小的并且新的大小小于或等于該 ArrayBuffer 的 maxByteLength。
const buffer = new ArrayBuffer(8, { maxByteLength: 16 });
console.log(buffer.byteLength);
// 8
if (buffer.resizable) {
console.log("緩沖區大小是可調整的!");
buffer.resize(12);
}
注意:
- 如果 ArrayBuffer 已分離或不可調整大小,則拋出該錯誤。
- 如果 newLength 大于該 ArrayBuffer 的 maxByteLength,則拋出該錯誤。
ArrayBuffer.prototype.transfer
transfer() 方法執行與結構化克隆算法相同的操作。它將當前 ArrayBuffer 的字節復制到一個新的 ArrayBuffer 對象中,然后分離當前 ArrayBuffer 對象,保留了當前 ArrayBuffer 的大小可調整性。
// 創建一個 ArrayBuffer 并寫入一些字節
const buffer = new ArrayBuffer(8);
const view = new Uint8Array(buffer);
view[1] = 2;
view[7] = 4;
// 將緩沖區復制到另一個相同大小的緩沖區
const buffer2 = buffer.transfer();
console.log(buffer.detached); // true
console.log(buffer2.byteLength); // 8
const view2 = new Uint8Array(buffer2);
console.log(view2[1]); // 2
console.log(view2[7]); // 4
// 將緩沖區復制到一個更小的緩沖區
const buffer3 = buffer2.transfer(4);
console.log(buffer3.byteLength); // 4
const view3 = new Uint8Array(buffer3);
console.log(view3[1]); // 2
console.log(view3[7]); // undefined
// 將緩沖區復制到一個更大的緩沖區
const buffer4 = buffer3.transfer(8);
console.log(buffer4.byteLength); // 8
const view4 = new Uint8Array(buffer4);
console.log(view4[1]); // 2
console.log(view4[7]); // 0
// 已經分離,拋出 TypeError
buffer.transfer(); // TypeError: Cannot perform ArrayBuffer.prototype.transfer on a detached ArrayBuffer
String.prototype.isWellFormed()
isWellFormed() 方法返回一個表示該字符串是否包含單獨代理項的布爾值。
JavaScript 中的字符串是 UTF-16 編碼的。UTF-16 編碼中的代理對是指:
在UTF-16編碼中,代理對(Surrogate Pair)是一種特殊的編碼機制,用于表示那些超出基本多文種平面(BMP)的Unicode字符。這些字符的Unicode碼點高于U+FFFF,因此無法用一個16位的UTF-16碼元來表示。為了解決這個問題,UTF-16引入了代理對機制。
代理對是由兩個16位的碼元組成的,一個稱為高代理(或高代理碼元),其碼點范圍在U+D800到U+DBFF之間;另一個稱為低代理(或低代理碼元),其碼點范圍在U+DC00到U+DFFF之間。這兩個碼元合在一起,可以表示一個超出BMP的Unicode字符。
例如,Unicode碼點U+10000(這是BMP之外的第一個碼點)在UTF-16中的編碼就是高代理碼元U+D800和低代理碼元U+DC00的組合,即“D800 DC00”。同樣,碼點U+10001的UTF-16編碼就是“D800 DC01”,以此類推。
通過這種方式,UTF-16編碼能夠完全表示所有Unicode字符,無論是BMP內的還是BMP外的。這種代理對機制是UTF-16編碼方案的一個重要組成部分,使得UTF-16成為一種能夠靈活處理各種語言字符的編碼方式。
isWellFormed() 讓你能夠測試一個字符串是否是格式正確的(即不包含單獨代理項)。由于引擎能夠直接訪問字符串的內部表示,與自定義實現相比 isWellFormed() 更高效。如果需要將字符串轉換為格式正確的字符串,可以使用 toWellFormed() 方法。isWellFormed() 讓你可以對格式正確和格式錯誤的字符串進行不同的處理,比如拋出一個錯誤或將其標記為無效。
const strings = [
// 單獨的前導代理
"ab\uD800",
"ab\uD800c",
// 單獨的后尾代理
"\uDFFFab",
"c\uDFFFab",
// 格式正確
"abc",
"ab\uD83D\uDE04c",
];
for (const str of strings) {
console.log(str.isWellFormed());
}
// 輸出:
// false
// false
// false
// false
// true
// true
如果傳遞的字符串格式不正確, encodeURI 會拋出錯誤。可以通過使用 isWellFormed() 在將字符串傳遞給 encodeURI() 之前測試字符串來避免這種情況。
const illFormed = "https://example.com/search?q=\uD800";
try {
encodeURI(illFormed);
} catch (e) {
console.log(e); // URIError: URI malformed
}
if (illFormed.isWellFormed()) {
console.log(encodeURI(illFormed));
} else {
console.warn("Ill-formed strings encountered."); // Ill-formed strings encountered.
}
String.prototype.toWellFormed()
toWellFormed() 方法返回一個字符串,其中該字符串的所有單獨代理項都被替換為 Unicode 替換字符 U+FFFD。
toWellFormed() 迭代字符串的碼元,并將任何單獨代理項替換為 Unicode 替換字符 U+FFFD。這確保了返回的字符串格式正確并可用于期望正確格式字符串的函數,比如 encodeURI。由于引擎能夠直接訪問字符串的內部表示,與自定義實現相比 toWellFormed() 更高效。
const strings = [
// 單獨的前導代理
"ab\uD800",
"ab\uD800c",
// 單獨的后尾代理
"\uDFFFab",
"c\uDFFFab",
// 格式正確
"abc",
"ab\uD83D\uDE04c",
];
for (const str of strings) {
console.log(str.toWellFormed());
}
// Logs:
// "ab?"
// "ab?c"
// "?ab"
// "c?ab"
// "abc"
// "ab??c"
如果傳遞的字符串格式不正確, encodeURI 會拋出錯誤。可以先通過使用 toWellFormed() 將字符串轉換為格式正確的字符串來避免這種情況。
const illFormed = "https://example.com/search?q=\uD800";
try {
encodeURI(illFormed);
} catch (e) {
console.log(e); // URIError: URI malformed
}
console.log(encodeURI(illFormed.toWellFormed())); // "https://example.com/search?q=%EF%BF%BD"
Atomics.waitAsync()
Atomics.waitAsync() 靜態方法異步等待共享內存的特定位置并返回一個 Promise。與 Atomics.wait() 不同,waitAsync 是非阻塞的且可用于主線程。
下面來看一個簡單的例子,給定一個共享的 Int32Array。
const sab = new SharedArrayBuffer(1024);
const int32 = new Int32Array(sab);
令一個讀取線程休眠并在位置 0 處等待,預期該位置的值為 0。result.value 將是一個 promise。
const result = Atomics.waitAsync(int32, 0, 0, 1000);
// { async: true, value: Promise {<pending>} }
在該讀取線程或另一個線程中,對內存位置 0 調用以令該 promise 解決為 "ok"。
Atomics.notify(int32, 0);
// { async: true, value: Promise {<fulfilled>: 'ok'} }
如果它沒有解決為 "ok",則共享內存該位置的值不符合預期(value 將是 "not-equal" 而不是一個 promise)或已經超時(該 promise 將解決為 "time-out")。
正則表達式 v 標志
RegExp v 標志是 u 標志的超集,并提供了另外兩個功能:
- 字符串的 Unicode 屬性: 通過 Unicode 屬性轉義,可以使用字符串的屬性。
const re = /^\p{RGI_Emoji}$/v;
// 匹配僅包含 1 個代碼點的表情符號:
re.test('?'); // '\u26BD'
// → true ?
// 匹配由多個代碼點組成的表情符號:
re.test('??????'); // '\u{1F468}\u{1F3FE}\u200D\u2695\uFE0F'
// → true ?
- 設置符號: 允許在字符類之間進行集合操作。
const re = /[\p{White_Space}&&\p{ASCII}]/v;
re.test('\n'); // → true
re.test('\u2028'); // → false
在JavaScript的正則表達式中,u 標志表示“Unicode”模式。當你在正則表達式中使用這個標志時,它會將模式視為Unicode序列的集合,而不僅僅是一組ASCII字符。這意味著正則表達式會正確地處理四個字節的UTF-16編碼。
具體來說,u 標志有以下幾個作用:
正確處理Unicode字符:不使用 u 標志時,正則表達式可能無法正確處理Unicode字符,尤其是那些超出基本多文種平面(BMP)的字符。使用 u 標志后,你可以匹配和處理任何有效的Unicode字符。
改變量詞的行為:在Unicode模式下,量詞(如 *、+、?、{n}、{n,}、{n,m})會匹配任何有效的Unicode字符,而不僅僅是ASCII字符。
允許使用\p{...} 和 \P{...}:這兩個是Unicode屬性轉義,允許匹配或不匹配具有特定Unicode屬性的字符。例如,\p{Script=Arabic} 會匹配任何阿拉伯腳本的字符。
正確處理Unicode轉義:在Unicode模式下,你可以使用 \u{...} 來表示一個Unicode字符,其中 {...} 是一個四位的十六進制數。
修正了某些正則表達式方法的行為:例如,String.prototype.match()、String.prototype.replace()、String.prototype.search() 和 RegExp.prototype.exec() 等方法在Unicode模式下會返回更準確的結果。