Are You Ready? ES2022!

本系列文章经过重新编排和扩充,已出书为ECMAScript关键30天。原始文章因当时准备时程紧迫,多少有些许错误。为了避免造成读者的困扰,以及配合书籍的内容规划,将会陆续更新本系列文章。
本篇文章在 2021/11/1 已更新,刊载在我的Blog

前言

ECMAScript 原本是不定期地释出版本,但因应提案的踊跃和开发需求的迫切,所以从 ES2015 後就改为一年一修。也就是说每年都会有新的语法标准出现,让开发者可以使用更简洁弹性的语法撰写,或是实现更强大的功能。

接下来整理预计会在 ES2022 释出的提案,以简单的说明加上清楚的程序范例,快速了解起手式?

ECMAScript 提案五阶段

一个想法出来到纳入提案,接着成为修订标准,通常会经历以下 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 中占了蛮大一部分的内容。其中可以把这些修订分类为-

  • 扩充静态关键字(static)的使用
  • 正式支援私有(private)的机制

静态方法与成员(Static methods and fields)

在 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: 公开静态成员

静态初始化区块(Static initialization blocks)

类别初始化时,有些静态成员需要透过流程控制设定初始值的话,通常只能在类别建立後,把相关的初始流程放在之後。在程序的语意上会像是两个独立的区块拼在一起,规模越复杂的话,就会降低维护性。

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 };
        }
    }
}

定义私有方法与成员(Private methods and fields)

成为私有成员,表示只有内部可以使用,外部如果尝试存取或呼叫的话,就会回传 undefined 或错误提示。这样做可以保护成员不会被外部任意修改,或设定只有内部可以操作的方法等。

在 ES2022 後,正式把 # 作为私有成员的前缀字。像是上面的范例程序-static #defaultRoot = 'myroot/api';defaultRoot 属性就是一种私有属性。

需要注意的是,不只是宣告,在呼叫方法或存取属性时也需要冠上这个前缀字。更多有关私有方法与成员的范例程序与说明,可以参考我即将上市的书-ECMAScript关键30天

顶层的 await

awaitasync 的语法是非同步处理的语法糖。只要函式中有 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,并且设定 @babel/preset-env 这个 preset 的话,就可以试试看目前有支援的转换语法,像是顶层 await、类别的静态初始化区块等。

TypeScript

如果专案是以 TypeScript 开发,在目前的 beta 版可以透过 tsconfig.jsonmodule 设定来支援顶层 await。

小结

这篇文章的内容,主要是撷取自我即将上市的书。如果有兴趣的话,也欢迎在 11/12 後去天珑翻翻看,或是在博客来等网站翻一下试阅页。有对到你的电波的话,欢迎把它带回家喔?

这篇内容也有影片版可以观看。在这里也谢谢 JSDC 的邀请,让我有了直播技术分享的初体验。另外 JSDC 当天的议程内容真的很充实,受益良多。有时间的话,会再陆续整理相关心得。

参考连结


<<:  Day29 - Float

>>:  I Want To Know React - PropTypes & DefaultProps

A First Set of Refactorings

本篇同步发文於个人网站: A First Set of Refactorings This arti...

Day28 NiFi 案例分享 - Renault

今天这篇来分享一个我觉得在介绍 Apache NiFi 的时候很典型的一个企业案例 - Renaul...

[Day18] POPCAT in WASM (Part 2)

好 那今天就会完成这个小专案 可能 CSS 的部份写的没有很好 ouo 读者可以自行修改 还是再放一...

Day11. 活用 Ruby Class

Class 是Ruby很重要的观念,要学习 Ruby 的一定要学会class & 物件。我们...

CSS微动画 - 弹出来的选单 Part.1

Q: 这个看起来像猫爪的东西是什麽? A: 喵? 本篇开始将实作选单的微动画,比较特别的要来说说t...