ES2024 (ES15) 已在今年 (2024) 7 月正式發佈。這個版本帶來了許多令人興奮且等待已久的功能,也讓 js 朝現代化語言更前進了一步。完整的官方標準請見 ECMAScript® 2024 Language Specification。
如果您想先回顧 ES2020-2023 的改變內容,可以先參考我們過去的文章:
而今天,就讓夏格飛來替大家導覽 ES2024 的全新功能吧,這次不只加入了社群期待以久的重要功能,也包含一些可能改變未來寫法的新特性以及語法,讓我們看下去。
- Promise.withResolvers()
- groupBy()
- Unicode 字串的 toWellFormed() 字串格式化
- Atomic.waitAsync()
- 正規表示式新修飾符: v 與字串操作
- 頂層 await (Top-level await)
- 管道 (Pipeline) 運算符
- 裝飾器 (Decorator)
- 取代 Date 的 Temporal API
- 強化的私有屬性
- Realms API 領域隔離
- 總結
本文章目次
Promise.withResolvers()
Promise 終於可以簡單的從外部進行 resolve 了,我們不用再自己落落長的寫 Workaround 或是用第三方 Deferred 物件來處理了。
以前
let resolveIt;
new Promise((resolve) => {
resolveIt = resolve;
});
resolveIt() // To resolve
現在
const { promise, resolve, reject } = Promise.withResolvers();
用在某些 callback 時 resolve Promise 非常方便
const { promise, resolve } = Promise.withResolvers();
// Some event
foo.addEventListener('load', () => {
resolve();
});
await promise;
groupBy()
新的 Object.groupBy()
與 Map.groupBy()
可以讓我們快速群組一系列物件。
// Create an Array
const fruits = [
{name: "apples", quantity: 300},
{name: "bananas", quantity: 500},
{name: "oranges", quantity: 200},
{name: "kiwi", quantity: 150}
];
const result = Object.groupBy(fruits, function ({ quantity }) {
return quantity > 200 ? "ok" : "low";
});
/*
{
"ok": [
{
"name": "apples",
"quantity": 300
},
{
"name": "bananas",
"quantity": 500
}
],
"low": [
{
"name": "oranges",
"quantity": 200
},
{
"name": "kiwi",
"quantity": 150
}
]
}
*/
Unicode 字串的 toWellFormed() 字串格式化
Unicode 字串對於表示來自不同語言和符號的各種字元非常重要。此次更新將確保在不同的 JavaScript 環境中一致且準確地處理這些字串。
在過去,只要遇到無效的 Unicode 字元,在顯示時都會轉成 �
也就是 U+FFFD
來顯示,稱為 surrogate 。但字串本身的編碼還是沒有變,所以當遇到某些函式要處理字串時,就會跳出 Malformed unicode character 之類的錯誤訊息。
而 toWellFormed()
可以將字串中任何不正確的 Unicode 代碼統一轉成 U+FFFD
,這樣各類字串函式就可以正常處理了。
const sampleStrings = [
// 各種帶有 surrogate 的異常範例
"igor\uD800",
"igor\uD800komolov",
"\uDC00yourfuse",
"your\uDC00fuse",
// 正確格式
"yourFuse",
"emoji\uD83D\uDE00",
];
sampleStrings.forEach(str => {
console.log(`Processed String: ${str.toWellFormed()}`);
});
// Expected output:
// "Processed String: igor�"
// "Processed String: igor�komolov"
// "Processed String: �yourfuse"
// "Processed String: your�fuse"
// "Processed String: yourFuse"
// "Processed String: emoji😀"
上面的範例利用 toWellFormed()
將字串中有錯誤的 Unicode 轉換成固定的 �
。這確保返回的字串格式良好,並且可以在需要格式良好的字串的函數中使用。
下面的範例展示了原本常用的 encodeURI()
遇到錯誤 Unicode 時,會直接跳 Error,但先使用 toWellFormed()
可以有效的轉成可用字元(U+FFFD
編碼為%EF%BF%BD
)
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"
Atomic.waitAsync()
他是對應 Atomic.wait()
的功能,原本的 Atomic.wait()
是用來等待一個 SharedArray 中的某個位置返回 promise,主要用在共享記憶體的情境下使用,稱為原子操作,參見 MDN Atomic。
而原本的 Atomic.wait()
是 sync 的,會造成 UI 介面鎖死,所以不太能用在瀏覽器上。新的 Atomic.waitAsync()
將可以轉成異步等待,不再鎖死畫面。
const sab = new SharedArrayBuffer(1024);
const int32 = new Int32Array(sab);
const result = Atomics.waitAsync(int32, 0, 0, 1000);
// { async: true, value: Promise {<pending>} }
Atomics.notify(int32, 0);
// { async: true, value: Promise {<fulfilled>: 'ok'} }
正規表示式新修飾符: v 與字串操作
JavaScript 中正規表示式的這種改進允許更複雜的模式匹配和字串操作。 「v」修飾符和字串操作符號可實現更精確和更具可讀性的正規表示式。例如,您可以使用此功能來匹配具有特定 Unicode 屬性的一組字元。
// 字串 diff 或排除
[A--B]
// 字串交集
[A&&B]
// 嵌套字元類別
[A--[0-9]]
頂層 await (Top-level await)
終於,等了很久的 Top-level await 成為正式標準了。在過去幾年早就被用在無數專案了,由於這個性質不能用 Babel Polyfill,所以在現代化瀏覽器紛紛提早支援的情況下,一旦用了這個功能就表示完全放棄 IE 支援。而現在則是成為正式標準了。
// With top-level await
const data = await fetchData();
console.log(data);
管道 (Pipeline) 運算符
管道運算符 (|>)
透過多個函數呼叫提高了程式碼的可讀性。它允許使用函數式語法,其中表達式的結果作為參數傳遞給下一個函數。例如,您可以將巢狀函數呼叫重構為可讀性高的操作序列:
// Before
const calculatedValue = Math.ceil(Math.pow(Math.max(0, -10), 1/3));
// After
const calculatedValue = -10
|> (n => Math.max(0, n))
|> (n => Math.pow(n, 1/3))
|> Math.ceil;
雖然很多函式本來就支持鏈狀呼叫,例如下面陣列的鏈狀呼叫:
arr.filter(...)
.map(...)
.join(',')
但是管道操作符進一步讓鏈狀呼叫擴及到所有函式上面,對未來的開發手法會造成廣泛的影響。
Record 與 Tuple 型別
Record 與 Tuple 分別是物件與陣列的不可變版本,創建後便無法修改。更新 Record 或 Tuple 會產生新物件:
// 建立 Record
const userProfile = #{
username: "IgorKomolov",
age: 39,
};
// 建立 Tuple
const numberSequence = #[10, 20, 30];
// 更新他們,將回傳新實例
const updatedProfile = userProfile.with({ age: 40});
console.log(updatedProfile); // #{ username: "IgorKomolov", age: 40 }
console.log(userProfile); // #{ username: "IgorKomolov", age: 39 } (原本的不變)
const newNumberSequence = numberSequence.with(1, 25);
console.log(newNumberSequence); // #[10, 25, 30]
console.log(numberSequence); // #[10, 20, 30] (原本的不變)
裝飾器 (Decorator)
認真,這個大家用到爛的功能現在才正式推出?
總而言之,感謝 TypeScript 預先帶來這個機制,多年後 JS 終於以相近的語法推出了裝飾器,現在開箱即用!
這功能允許修改或增強類別、方法、屬性或參數的行為。它們對於以聲明方式添加元資料、日誌記錄或修改行為特別有用:
@Component
class SampleClass {
@TrackExecution
action(parameter1, parameter2) {
//
}
}
另外要注意,function 無法掛上裝飾器喔。原因是 function 是 JS 一級公民,在初始化編譯時就會提早宣告了,所以裝飾器來不及在 function 上面生效。這個提早宣告的行為稱為 Hoist,也是為什麼 function 可以宣告在程式碼下面的原因。
相關文章可以參考 ECMAScript decorators on functions
取代 Date 的 Temporal API
Temporal 已經成為草案了一段時間,是為 JavaScript 提出的現代且全面的日期時間 API,目的是處理許多現有的 Date
難以涵蓋的複雜時間處理功能。下面是 Temporal 的一些範例:
取得 UTC 的當下時刻
Temporal.Now.instant().toString()
取得特定時區中的目前分區日期時間
Temporal.Now.zonedDateTimeISO('Asia/Taipei').toString()
取得 ISO 格式的目前純日期時間
Temporal.Now.plainDateTimeISO().toString()
取得 ISO 格式的目前普通時間
Temporal.Now.plainTimeISO().toString()
。
關於 ZonedDateTime
Temporal 中的類別ZonedDateTime
具有多個屬性和方法,允許對日期時間資訊進行詳細操作和檢索。
其中包括日曆、時區、年、月、日、小時、分鐘、秒甚至奈秒的 getter。
它還包括
.with()
、.add()
、.subtract()
、.until()
、.since()
和等方法.round()
,提供了處理分區日期時間值的廣泛功能。
Temporal 中的 Plain 時間類別
Temporal 引入了「Plain」類別,它們是沒有時區的時間的抽象表示。
這些類別包括
PlainDateTime
、PlainDate
和PlainTime
。它們對於顯示給定時區的時間或與時區無關的時間計算非常有用,例如尋找 1984 年 6 月的第一個星期二。
這些範例示範了 ES2024 中的 Temporal 如何簡化和增強 JavaScript 中的日期時間處理,為開發人員提供更強大、更通用的工具。
const now = Temporal.Now.zonedDateTimeISO('America/New_York');
console.log(now.toString());
const date = Temporal.PlainDate.from('2024-01-01');
const newDate = date.add({ days: 10 });
console.log(newDate.toString()); // Outputs '2024-01-11'
更多介紹
強化的私有屬性
現在,私有屬性不必使用 constructor 定義了,可以直接宣告
class User {
username;
dateOfBirth;
#address;
}
也可以建立 getter
class User {
username;
dateOfBirth;
#address;
get #address() {
return #address;
}
}
同時簡化了物件類型的檢查,使類型檢查更加直覺且不易出錯。下面的範例,展示了函式可能要檢查不同類型的物件是否包含私有屬性欄位,由於物件可能不同,只能用 try catch 處理。
class Book {
#author;
constructor(author) {
this.#author = author;
}
static hasAuthorField(obj) {
try {
obj.#author; // 確認物件的私有屬性是否存在
return true;
} catch (err) {
if (err instanceof TypeError) {
return false; // 不存在
}
throw err; // 錯誤
}
}
}
// 示範
const myBook = new Book("Igor Komolov");
console.log(Book.hasAuthorField(myBook)); // 應該要是 true
const otherObject = {};
console.log(Book.hasAuthorField(otherObject)); // 應該要是 false
ES2024 之後,可以用 in
檢查
class BookES2024 {
#author;
constructor(author) {
this.#author = author;
}
static hasAuthorField(obj) {
return #author in obj; // 新的 2024 私有屬性檢查方式
}
}
Realms API 領域隔離
此 API 提供了一種建立隔離 JavaScript 環境的機制。它對於安全程式碼執行和沙箱非常有用,允許您在受控和隔離的環境中運行程式碼。(領域展開?)
展開我的領域吧!
const myRealm = new Realm();
myRealm.evaluate('3 * 5'); // 15
共享 Symbols
const myRealm = new Realm();
Symbol.for('y') === myRealm.evaluate('Symbol.for("y")'); // true
包裹成函式
const myRealm = new Realm();
const doubleFunction = myRealm.evaluate('num => num * 2');
doubleFunction(10); // 20
送入 callback
const myRealm = new Realm();
const processNumber = myRealm.evaluate('(number, callback) => callback(number + 5)');
processNumber(5, (result => console.log(result))); // Logs 10 (5 + 5)
不可存取全域物件
const myRealm = new Realm();
myRealm.evaluate('this'); // Throws a TypeError
myRealm.evaluate('new Array()'); // Throws a TypeError
myRealm.evaluate('Object.keys({})'); // Throws a TypeError
這功能很適合替代 eval() 作為客製化語法的核心功能,可以預見他能讓許多模版套件的程式碼減少許多。
總結
這次 ES2024 的新特性比往年一口氣加入的多,還都是許多等待以久,大家偷跑用了好幾年的功能,如 top-level await 與 decorator。也包含許多將改變未來編寫模式的特性,如 Promise.withResolvers、pipelines 與 Temporal 。
我們可以預期的是 ECMAScript 持續在朝更新,更現代化的方向走。而 Node.js 與 TypeScript 的整合,也帶領我們前往更安全、更嚴謹的路線。2024 以後的 Javascript 如何發展,值得我們拭目以待。