本系列文章經過重新編排和擴充,已出書為ECMAScript關鍵30天。原始文章因當時準備時程緊迫,多少有些許錯誤。為了避免造成讀者的困擾,以及配合書籍的內容規劃,將會陸續更新本系列文章。
本篇文章在 2021/11/1 已更新,刊載在我的Blog
ECMAScript 原本是不定期地釋出版本,但因應提案的踴躍和開發需求的迫切,所以從 ES2015 後就改為一年一修。也就是說每年都會有新的語法標準出現,讓開發者可以使用更簡潔彈性的語法撰寫,或是實現更強大的功能。
接下來整理預計會在 ES2022 釋出的提案,以簡單的說明加上清楚的程式範例,快速了解起手式?
一個想法出來到納入提案,接著成為修訂標準,通常會經歷以下 5 個階段-
Stage | 說明 |
---|---|
0 | 由 TC-39 的成員或其他人提出後,委員會沒有否決的討論或想法 |
1 | 正式成為提案,需有具體語法和描述,部分會有Polyfill的實現 |
2 | 可能有相關的運作環境或編譯器提供實驗性的功能實現 |
3 | 成為候選提案,部分運作環境或編譯器提供原生支援 |
4 | 通過至少兩個驗收測試,等待下版釋出時成為修訂內容 |
今天要講的語法,都是已經進到 finished,也就是 Stage 4 階段的提案。有興趣的話,可以前往 TC-39 的 GitHub 看相關整理。有趣的是,在這裡不僅可以看到語法的開發動機和具體描述,也能看到在提案推進的過程中,開發者與委員們的討論紀錄,可以更進一步地了解標準推出的始末。
先簡單複習一下。正規表達式主要有以下三種組成-
其中旗標的部分,一個 RegExp 物件會針對旗標提供實體屬性來查詢。透過取得屬性值,就可以知道這個正規表達式有沒有設定對應的旗標。
Stage | 對應屬性 | 說明 |
---|---|---|
d | hasIndices | 回傳每個匹配內容的起始&下次要開始檢索的索引陣列([startIndex, endIndex+1] ) |
有加上 d
旗標的正規表達式,在執行匹配相關的操作,例如 exec
時,可以多回傳一個 indices
屬性。這個屬性會包含匹配內容的起始索引,以及結束索引加 1,也就是視為下次要開始檢索的索引。
目前最新版本的 Chrome 還有Node.js中可以運作,有興趣的話可以嘗試看看。
const myRegexp = /\w*.o\w+/dgi; // 找出中間有 o 的單字
const target = 'Born to make history';
let currResult;
while ((currResult = myRegexp.exec(target)) !== null) {
console.log('這次的符合內容: ', currResult[0]);
console.log('Matched Indices: ', currResult.indices[0]);
console.log('---');
}
// 這次的符合內容: Born
// Matched Indices: (2) [0, 4]
// ---
// 這次的符合內容: history
// Matched Indices: (2) [13, 20]
// ---
常見的這類物件有字串跟陣列。那麼在 ES2022 推出了什麼相關語法呢?
indexable.at(index)
用途跟使用字面值取得元素的方式類似,透過索引的傳入來獲得元素。不過索引值在負整數跟浮點數時,跟字面值的回傳結果會不一樣。可以看一嚇得比較表-
取得元素的方式 | 負整數 | 浮點數 |
---|---|---|
indexable[index] |
undefined |
undefined |
indexable.at(index) |
後面從 -1 開始算 | 無條件捨去後,再由正負數決定查找方向 |
目前最新版本的 Chrome 可以運作,有興趣的話可以嘗試看看。
const myArray = [0, 1, 2, 3];
console.log(myArray[-2], myArray.at(-2)); // undefined 2
console.log(myArray[1.6], myArray.at(1.6)); // undefined 1
console.log(myArray[-3.2], myArray.at(-3.2)); // undefined 1
Object.hasOwn(target, propName)
用來檢查目標物件有沒有屬性。那跟目前習慣使用的 hasOwnProperty
有什麼不一樣呢?
假設以 Object.create(null)
建立了空物件,然後新增一些屬性後,我們想檢查這個物件有沒有特定屬性。可是物件的原型指向了 null
,所以就無法使用實體方法 hasOwnProperty
,除非要直接呼叫 Object.prototype.hasOwnProperty
加上 call
方法來達到這個目的。不過這樣的寫法太冗長了。因此 ES2022 就在 Object
底下新增這個靜態方法,成為語法糖。
目前最新版本的 Chrome 可以運作,有興趣的話可以嘗試看看。
// before
const hasNameProp = Object.prototype.hasOwnProperty.call(myObject, 'name');
// after
const hasNameProp = Object.hasOwn(myObject, 'name');
類別的相關語法在 ES2022 中占了蠻大一部分的內容。其中可以把這些修訂分類為-
在 ES2015 時提出的 static
關鍵字,只能做為公開靜態方法的前綴字。不過在 ES2022 後,static
關鍵字的應用範圍更廣泛了。無論是屬性或方法,公開或私有,只要在名稱前加上前綴字,就能表示為靜態。
目前最新版本的 Chrome 可以運作,有興趣的話可以嘗試看看。
class Inbody {
static #secretNumber = 1.2; // ES2022: 私有靜態成員
static brand = 'My Inbody'; // ES2022: 公開靜態成員
// ES2022: 私有靜態方法
static #getPBF(weight, fat) {
return ((fat * Person.#secretNumber) / weight) * 100;
}
// ES2015: 公開靜態方法
static getBMI(weight, height) {
return weight / Math.pow(height / 100, 2);
}
}
Inbody.country = 'Taiwan'; // ES2015: 公開靜態成員
類別初始化時,有些靜態成員需要透過流程控制設定初始值的話,通常只能在類別建立後,把相關的初始流程放在之後。在程式的語意上會像是兩個獨立的區塊拼在一起,規模越複雜的話,就會降低維護性。
class APILibrary {
static configs;
}
try {
const fetchedConfigs = await fetchConfigs();
APILibrary.configs = { ...fetchedConfigs, tag: 1 };
} catch (error) {
APILibrary.configs = { root: 'myroot/api', tag: 2 };
}
ES2022後,類別中只要使用 static
關鍵字加上大括號({ }
),就能包覆靜態成員的初始流程。這樣做還有個好處是,流程中需要存取類別的私有成員時,可以直接取用,而不用再撰寫額外存取器方法來封裝,寫法上算是優雅很多。
class APILibrary {
static configs;
static #defaultRoot = 'myroot/api';
static {
try {
const fetchedConfigs = await fetchConfigs();
APILibrary.configs = { ...fetchedConfigs, tag: 1 };
} catch (error) {
APILibrary.configs = { root: APILibrary.#defaultRoot, tag: 2 };
}
}
}
成為私有成員,表示只有內部可以使用,外部如果嘗試存取或呼叫的話,就會回傳 undefined
或錯誤提示。這樣做可以保護成員不會被外部任意修改,或設定只有內部可以操作的方法等。
在 ES2022 後,正式把 #
作為私有成員的前綴字。像是上面的範例程式-static #defaultRoot = 'myroot/api';
,defaultRoot
屬性就是一種私有屬性。
需要注意的是,不只是宣告,在呼叫方法或存取屬性時也需要冠上這個前綴字。更多有關私有方法與成員的範例程式與說明,可以參考我即將上市的書-ECMAScript關鍵30天。
await
跟 async
的語法是非同步處理的語法糖。只要函式中有 await
的關鍵字出現,就一定要在該函式名稱的前面加 async
的前綴字。在 ES2022 後,可以允許在需要非同步的地方加上 await
就好,不需使用async函式封裝,提升了撰寫非同步的彈性與簡潔性。
// case 1
import { fetchAppConfig } from '../APILibrary';
const appConfig = await fetchAppConfig();
// case 2
const menuData = fetch('api/config/menu.json').then((response) =>
response.json()
);
export default await menuData;
如果有在專案中使用 Babel,並且設定 @babel/preset-env
這個 preset 的話,就可以試試看目前有支援的轉換語法,像是頂層 await、類別的靜態初始化區塊等。
如果專案是以 TypeScript 開發,在目前的 beta 版可以透過 tsconfig.json
的 module
設定來支援頂層 await。
這篇文章的內容,主要是擷取自我即將上市的書。如果有興趣的話,也歡迎在 11/12 後去天瓏翻翻看,或是在博客來等網站翻一下試閱頁。有對到你的電波的話,歡迎把它帶回家喔?
這篇內容也有影片版可以觀看。在這裡也謝謝 JSDC 的邀請,讓我有了直播技術分享的初體驗。另外 JSDC 當天的議程內容真的很充實,受益良多。有時間的話,會再陸續整理相關心得。