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