close

2022 年 6 月 22 日,第 123 屆 ECMA 大會批准了 ECMAScript 2022 語言規範,這意味着它現在正式成為標準。下面就來看看 ECMAScript 2022 有哪些新特性!

總覽:

Top-level Await
Object.hasOwn()
at()
error.cause
正則表達式匹配索引
1. Top-level Await

在ES2017中,引入了 async 函數和 await 關鍵字,以簡化 Promise 的使用,但是 await 關鍵字只能在 async 函數內部使用。嘗試在異步函數之外使用 await 就會報錯:SyntaxError - SyntaxError: await is only valid in async function。

頂層 await 允許我們在 async 函數外面使用 await 關鍵字。它允許模塊充當大型異步函數,通過頂層 await,這些 ECMAScript 模塊可以等待資源加載。這樣其他導入這些模塊的模塊在執行代碼之前要等待資源加載完再去執行。

由於 await 僅在 async 函數中可用,因此模塊可以通過將代碼包裝在 async 函數中來在代碼中包含 await:

//a.jsimportfetchfrom"node-fetch";letusers;exportconstfetchUsers=async()=>{constresp=awaitfetch('https://jsonplaceholder.typicode.com/users');users=resp.json();}fetchUsers();export{users};//usingAwait.jsimport{users}from'./a.js';console.log('users:',users);console.log('usingAwaitmodule');

我們還可以立即調用頂層async函數(IIAFE):

importfetchfrom"node-fetch";(async()=>{constresp=awaitfetch('https://jsonplaceholder.typicode.com/users');users=resp.json();})();export{users};

這樣會有一個缺點,直接導入的 users 是 undefined,需要在異步執行完成之後才能訪問它:

//usingAwait.jsimport{users}from'./a.js';console.log('users:',users);//undefinedsetTimeout(()=>{console.log('users:',users);},100);console.log('usingAwaitmodule');

當然,這種方法並不安全,因為如果異步函數執行花費的時間超過100毫秒, 它就不會起作用了,users 仍然是 undefined。

另一個方法是導出一個 promise,讓導入模塊知道數據已經準備好了:

//a.jsimportfetchfrom"node-fetch";exportdefault(async()=>{constresp=awaitfetch('https://jsonplaceholder.typicode.com/users');users=resp.json();})();export{users};//usingAwait.jsimportpromise,{users}from'./a.js';promise.then(()=>{console.log('usingAwaitmodule');setTimeout(()=>console.log('users:',users),100);});

雖然這種方法似乎是給出了預期的結果,但是有一定的局限性:導入模塊必須了解這種模式才能正確使用它。

而頂層await就可以解決這些問題:

//a.jsconstresp=awaitfetch('https://jsonplaceholder.typicode.com/users');constusers=resp.json();export{users};//usingAwait.jsimport{users}from'./a.mjs';console.log(users);console.log('usingAwaitmodule');

頂級 await 在以下場景中將非常有用:

動態加載模塊:
conststrings=awaitimport(`/i18n/${navigator.language}`);
資源初始化:
constconnection=awaitdbConnector();
依賴回退:
lettranslations;try{translations=awaitimport('https://app.fr.json');}catch{translations=awaitimport('https://fallback.en.json');}

該特性的瀏覽器支持如下:

2. Object.hasOwn()

在ES2022之前,可以使用 Object.prototype.hasOwnProperty() 來檢查一個屬性是否屬於對象。

Object.hasOwn 特性是一種更簡潔、更可靠的檢查屬性是否直接設置在對象上的方法:

constexample={property:'123'};console.log(Object.prototype.hasOwnProperty.call(example,'property'));console.log(Object.hasOwn(example,'property'));

該特性的瀏覽器支持如下:

3. at()

at() 是一個數組方法,用於通過給定索引來獲取數組元素。當給定索引為正時,這種新方法與使用括號表示法訪問具有相同的行為。當給出負整數索引時,就會從數組的最後一項開始檢索:

constarray=[0,1,2,3,4,5];console.log(array[array.length-1]);//5console.log(array.at(-1));//5console.log(array[array.lenght-2]);//4console.log(array.at(-2));//4

除了數組,字符串也可以使用at()方法進行索引:

conststr="helloworld";console.log(str[str.length-1]);//dconsole.log(str.at(-1));//d4. error.cause

在 ECMAScript 2022 規範中,new Error() 中可以指定導致它的原因:

functionreadFiles(filePaths){returnfilePaths.map((filePath)=>{try{//···}catch(error){thrownewError(`Whileprocessing${filePath}`,{cause:error});}});}5. 正則表達式匹配索引

該特性允許我們利用 d 字符來表示我們想要匹配字符串的開始和結束索引。以前,只能在字符串匹配操作期間獲得一個包含提取的字符串和索引信息的數組。在某些情況下,這是不夠的。因此,在這個規範中,如果設置標誌 /d,將額外獲得一個帶有開始和結束索引的數組。

constmatchObj=/(a+)(b+)/d.exec('aaaabb');console.log(matchObj[1])//'aaaa'console.log(matchObj[2])//'bb'

由於 /d 標識的存在,matchObj還有一個屬性.indices,它用來記錄捕獲的每個編號組:

console.log(matchObj.indices[1])//[0,4]console.log(matchObj.indices[2])//[4,6]

我們還可以使用命名組:

constmatchObj=/(?<as>a+)(?<bs>b+)/d.exec('aaaabb');console.log(matchObj.groups.as);//'aaaa'console.log(matchObj.groups.bs);//'bb'

這裡給兩個字符匹配分別命名為as和bs,然後就可以通過groups來獲取到這兩個命名分別匹配到的字符串。

它們的索引存儲在 matchObj.indices.groups 中:

console.log(matchObj.indices.groups.as);//[0,4]console.log(matchObj.indices.groups.bs);//[4,6]

匹配索引的一個重要用途就是指向語法錯誤所在位置的解析器。下面的代碼解決了一個相關問題:它指向引用內容的開始和結束位置。

constreQuoted=/「([^」]+)」/dgu;functionpointToQuotedText(str){conststartIndices=newSet();constendIndices=newSet();for(constmatchofstr.matchAll(reQuoted)){const[start,end]=match.indices[1];startIndices.add(start);endIndices.add(end);}letresult='';for(letindex=0;index<str.length;index++){if(startIndices.has(index)){result+='[';}elseif(endIndices.has(index+1)){result+=']';}else{result+='';}}returnresult;}console.log(pointToQuotedText('Theysaid「hello」and「goodbye」.'));//'[][]'6. 類 (1)公共實例字段

公共類字段允許我們使用賦值運算符 (=) 將實例屬性添加到類定義中。下面是一個計數器的例子:

importReact,{Component}from"react";exportclassIncrementorextendsComponent{constructor(){super();this.state={count:0,};this.increment=this.increment.bind(this);}increment(){this.setState({count:this.state.count+1});}render(){return(<buttononClick={this.increment}>Increment:{this.state.count}</button>);}}

在這個例子中,在構造函數中定義了實例字段和綁定方法,通過新的類語法,可以使代碼更加直觀。新的公共類字段語法允許我們直接將實例屬性作為屬性添加到類上,而無需使用構造函數方法。這樣就簡化了類的定義,使代碼更加簡潔、可讀:

importReactfrom"react";exportclassIncrementorextendsReact.Component{state={count:0};increment=()=>this.setState({count:this.state.count+1});render=()=>(<buttononClick={this.increment}>Increment:{this.state.count}</button>);}

有些小夥伴可能就疑問了,這個功能很早就可以使用了呀。但是它現在還不是標準的 ECMAScript,默認是不開啟的,如果使用 create-react-app 創建 React 項目,那麼它默認是啟用的,否則我們必須使用正確的babel插件才能正常使用(@babel/preset-env)。

下面來看看關於公共實例字段的注意事項:

公共實例字段存在於每個創建的類實例上。它們要麼是在Object.defineProperty()中添加,要麼是在基類中的構造時添加(構造函數主體執行之前執行),要麼在子類的super()返回之後添加:
classIncrementor{count=0}constinstance=newIncrementor();console.log(instance.count);//0
未初始化的字段會自動設置為 undefined:
classIncrementor{count}constinstance=newIncrementor();console.assert(instance.hasOwnProperty('count'));console.log(instance.count);//undefined
可以進行字段的計算:
constPREFIX='main';classIncrementor{[`${PREFIX}Count`]=0}constinstance=newIncrementor();console.log(instance.mainCount);//0(2)私有實例字段、方法和訪問器

默認情況下,ES6 中所有屬性都是公共的,可以在類外檢查或修改。下面來看一個例子:

classTimeTracker{name='zhangsan';project='blog';hours=0;setaddHours(hour){this.hours+=hour;}gettimeSheet(){return`${this.name}works${this.hours||'nothing'}hourson${this.project}`;}}letperson=newTimeTracker();person.addHours=2;//標準setterperson.hours=4;//繞過setter進行設置person.timeSheet;

可以看到,在類中沒有任何措施可以防止在不調用 setter 的情況下更改屬性。

而私有類字段將使用哈希#前綴定義,從上面的示例中,可以修改它以包含私有類字段,以防止在類方法之外更改屬性:

classTimeTracker{name='zhangsan';project='blog';#hours=0;//私有類字段setaddHours(hour){this.#hours+=hour;}gettimeSheet(){return`${this.name}works${this.#hours||'nothing'}hourson${this.project}`;}}letperson=newTimeTracker();person.addHours=4;//標準setterperson.timeSheet//zhangsanworks4hoursonblog

當嘗試在 setter 方法之外修改私有類字段時,就會報錯:

person.hours=4//ErrorPrivatefield'#hours'mustbedeclaredinanenclosingclass

還可以將方法或 getter/setter 設為私有,只需要給這些方法名稱前面加#即可:

classTimeTracker{name='zhangsan';project='blog';#hours=0;//私有類字段set#addHours(hour){this.#hours+=hour;}get#timeSheet(){return`${this.name}works${this.#hours||'nothing'}hourson${this.project}`;}constructor(hours){this.#addHours=hours;console.log(this.#timeSheet);}}letperson=newTimeTracker(4);//zhangsanworks4hoursonblog

由於嘗試訪問對象上不存在的私有字段會發生異常,因此需要能夠檢查對象是否具有給定的私有字段。可以使用 in 運算符來檢查對象上是否有私有字段:

classExample{#fieldstaticisExampleInstance(object){return#fieldinobject;}}(3)靜態公共字段

在ES6中,不能在類的每個實例中訪問靜態字段或方法,只能在原型中訪問。ES 2022 提供了一種在 JavaScript 中使用 static 關鍵字聲明靜態類字段的方法。下面來看一個例子:

classShape{staticcolor='blue';staticgetColor(){returnthis.color;}getMessage(){return`color:${this.color}`;}}

可以從類本身訪問靜態字段和方法:

console.log(Shape.color);//blueconsole.log(Shape.getColor());//blueconsole.log('color'inShape);//trueconsole.log('getColor'inShape);//trueconsole.log('getMessage'inShape);//false

實例不能訪問靜態字段和方法:

constshapeInstance=newShape();console.log(shapeInstance.color);//undefinedconsole.log(shapeInstance.getColor);//undefinedconsole.log(shapeInstance.getMessage());// color:undefined

靜態字段只能通過靜態方法訪問:

console.log(Shape.getColor());//blueconsole.log(Shape.getMessage());//TypeError:Shape.getMessageisnotafunction

這裡的 Shape.getMessage() 就報錯了,因為 getMessage 不是一個靜態函數,所以它不能通過類名 Shape 訪問。可以通過以下方式來解決這個問題:

getMessage(){return`color:${Shape.color}`;}

靜態字段和方法是從父類繼承的:

classRectangleextendsShape{}console.log(Rectangle.color);//blueconsole.log(Rectangle.getColor());//blueconsole.log('color'inRectangle);//trueconsole.log('getColor'inRectangle);//trueconsole.log('getMessage'inRectangle);//false(4)靜態私有字段和方法

與私有實例字段和方法一樣,靜態私有字段和方法也使用哈希 (#) 前綴來定義:

classShape{static#color='blue';static#getColor(){returnthis.#color;}getMessage(){return`color:${Shape.#getColor()}`;}}constshapeInstance=newShape();shapeInstance.getMessage();// color:blue

私有靜態字段有一個限制:只有定義私有靜態字段的類才能訪問該字段。這可能在使用 this 時導致出乎意料的情況:

classShape{static#color='blue';static#getColor(){returnthis.#color;}staticgetMessage(){return`color:${this.#color}`;}getMessageNonStatic(){return`color:${this.#getColor()}`;}}classRectangleextendsShape{}console.log(Rectangle.getMessage());//UncaughtTypeError:Cannotreadprivatemember#colorfromanobjectwhoseclassdidnotdeclareitconstrectangle=newRectangle();console.log(rectangle.getMessageNonStatic());//TypeError:Cannotreadprivatemember#getColorfromanobjectwhoseclassdidnotdeclareit

在這個例子中,this 指向的是 Rectangle 類,它無權訪問私有字段 #color。當我們嘗試調用 Rectangle.getMessage() 時,它無法讀取 #color 並拋出了 TypeError。可以這樣來進行修改:

classShape{static#color='blue';static#getColor(){returnthis.#color;}staticgetMessage(){return`${Shape.#color}`;}getMessageNonStatic(){return`color:${Shape.#getColor()}color`;}}classRectangleextendsShape{}console.log(Rectangle.getMessage());// color:blueconstrectangle=newRectangle();console.log(rectangle.getMessageNonStatic());// color:blue(5)類靜態初始化塊

靜態私有和公共字段只能讓我們在類定義期間執行靜態成員的每個字段初始化。如果我們需要在初始化期間像 try…catch 一樣進行異常處理,就不得不在類之外編寫此邏輯。該規範就提供了一種在類聲明/定義期間評估靜態初始化代碼塊的優雅方法,可以訪問類的私有字段。

先來看一個例子:

classPerson{staticGENDER="Male"staticTOTAL_EMPLOYED;staticTOTAL_UNEMPLOYED;try{//...}catch{//...}}

上面的代碼就會引發錯誤,可以使用類靜態塊來重構它,只需將try...catch包裹在 static 中即可:

classPerson{staticGENDER="Male"staticTOTAL_EMPLOYED;staticTOTAL_UNEMPLOYED;static{try{//...}catch{//...}}}

此外,類靜態塊提供對詞法範圍的私有字段和方法的特權訪問。這裡需要在具有實例私有字段的類和同一範圍內的函數之間共享信息的情況下很有用。

letgetData;classPerson{#xconstructor(x){this.#x={data:x};}static{getData=(obj)=>obj.#x;}}functionreadPrivateData(obj){returngetData(obj).data;}constjohn=newPerson([2,4,6,8]);readPrivateData(john);//[2,4,6,8]

這裡,Person 類與 readPrivateData 函數共享了私有實例屬性。

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()