ECMAScript 双月报告:Hashbang Grammer 提案成功进入到 Stage 4

语言: CN / TW / HK

作者 @穹心
审校 @昭朗

本次会议中,Hashbang Grammer 提案成功进入到 Stage 4,将在 ECMAScript 2023 中被作为正式语言特性加入到 JavaScript 当中。在上一次会议中获得了阶段性突破的 Duplicate named capturing groups 与 Import Reflection 提案,在本次会议中也再次实现了 Stage 的推进。除此以外,还有 Function Memoization 、Object.pick/omit 等在本次会议中首次推进到 Stage 1 的提案。

Stage 3 → Stage 4

从 Stage 3 进入到 Stage 4 有以下几个门槛:

  1. 必须编写与所有提案内容对应的 tc39/test262 [1] 测试,用于给各大 JavaScript 引擎和 transpiler 等实现检查与标准的兼容程度,并且 test262 已经合入了提案所需要的测试用例;

  2. 至少要有两个实现能够兼容上述 Test 262 测试,并发布到正式版本中;

  3. 发起了将提案内容合入正式标准文本 tc39/ecma262 [2] 的 Pull Request,并被 ECMAScript 编辑签署同意意见。

Hashbang Grammar

提案链接: proposal-hashbang [3]

Hashbang (也称 Shebang)语法常用于在类 Unix 系统下指定此脚本文件的解释器,它的语法大致是这样:

#!/usr/bin/env node
console.log("ecma");

JavaScript 作为一门解释性语言,其源码需要运行时将其解释为机器码才能运行,举例来说,使用 node :

$ node index.js

这一命令其实就指明了,我们在使用 node 来解释执行 index.js 文件。而“使用 node”这一信息,其实就可以通过上面的 Shebang 来将其内联到文件中,然后我们就可以直接运行此文件(需要 chmod +x index.js ):

$ ./index.js

/usr/bin/env 实际上是一个可执行程序,它将基于后面的参数为我们寻找实际程序,即 /usr/bin/env node 将指向操作系统上的 node 路径,这样我们就不需要自己写死 node 的安装路径了。

而此提案的主要作用在于,此前解释器所获得的 JS 代码是已经去除了 Shebang 的部分,而此提案会将 Shebang 的代码也完整地传递给引擎,由引擎层面来进行统一的标准化处理。

Stage 2 → Stage 3

提案从 Stage 2 进入到 Stage 3 有以下几个门槛:

  1. 撰写了包含提案所有内容的标准文本,并有指定的 TC39 成员审阅并签署了同意意见;

  2. ECMAScript 编辑签署了同意意见。

Duplicate named capturing groups

提案链接: proposal-duplicate-named-capturing-groups [4]

在正则表达式中,我们可以使用捕获组(Capturing Group)来对匹配模式中的某一部分做独立的匹配,如 es+ 会匹配 essssesssss+ 代表匹配一次或更多),而使用匹配组,我们可以将 es 作为一个匹配部分,如 (es)+ 会匹配 es 以及   eseses 等。

我们也可以对捕获组进行命名,如 ?<name> 这样的形式,常见的一个场景是结合 str.match 方法:

const dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/;
const str = "2022-06-01";

const groups = str.match(dateRegexp).groups;

groups.year; // 2022
groups.month; // 06
groups.day; // 01

无法使用同名捕获组匹配一组联合模式,如日期格式还可能是 06-01-2022,我们希望能这么使用联合模式:

const dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})|(?<day>[0-9]{2})-(?<month>[0-9]{2})-(?<year>[0-9]{4})/;

但由于捕获组的命名唯一约束,上面这个表达式是不合法的。

为了解决这一问题,此提案提出允许捕获组的命名不唯一,以此来支持如上面在联合模式中使用捕获组的场景。

Stage 1 → Stage 2

从 Stage 1 进入到 Stage 2 需要完成撰写包含提案所有内容的标准文本的初稿。

Import Reflection

提案链接: proposal-import-reflection [5]

Import Reflection 提案为 import 语句支持了在默认导入名前新增反射类型,来声明导入反射属性(元数据)的能力,其目前语法大致如下:

import module x from "<specifier>";

const x = await import("<specifier>", { reflect: "module" });

这里的 module 即为其反射类型。这一标注会改变 import 语句的对于目标模块的执行方式,以此提案的主要驱动场景之一为例, 为 WebAssembly 模块指定额外的类型,如实例导入( WebAssembly.Instance )与模块导入( WebAssembly.Module ):

import module FooModule from "./foo.wasm";
FooModule instanceof WebAssembly.Module; // true

// WASI 是适用于 WebAssembly 的模块化系统调用规范
import { WASI } from 'wasi';
const wasi = new WASI({ args, env, preopens });

const fooInstance = await WebAssembly.instantiate(FooModule, {
wasi_snapshot_preview1: wasi.wasiImport
});

wasi.start(fooInstance);

Stage 0 → Stage 1

从 Stage 0 进入到 Stage 1 有以下门槛:

  1. 找到一个 TC39 成员作为 champion 负责这个提案的演进;

  2. 明确提案需要解决的问题与需求和大致的解决方案;

  3. 有问题、解决方案的例子;

  4. 对 API 形式、关键算法、语义、实现风险等有讨论、分析。Stage 1 的提案会有可预见的比较大的改动,以下列出的例子并不代表提案最终会是例子中的语法、语义。

Symbol Predicates

提案链接: proposal-symbol-predicates [6]

此提案为 Symbol 顶级对象引入了两个新的方法: Symbol.isRegistered 与   Symbol.isWellKnown ,它们分别用于判断一个 Symbol 值是否已被注册,以及是否是 ECMA262 & ECMA402 规范中内置的 Symbol 类型(如 Symbol.iteratorSymbol.toPrimitive 等)。

这个提案主要是为了解决在 Symbol as WeakMap Key 提案中,仅有 Unique Symbol(直接通过 Symbol() 创建的 Symbol 值) 与 Well-known Symbol(内置 Symbol) 可以作为 WeakMap 结构 key 的问题。

你也可以使用这两个方法来判断一个 Symbol 类型是否是独一无二的:

const isUniqueSymbol = sym => typeof sym === "symbol" && !(Symbol.isRegistered(sym) || Symbol.isWellKnown(sym));

isUniqueSymbol(Symbol()); // true 一个新的 Symbol 类型
isUniqueSymbol(Symbol.for("foo")); // false Symbol.for 方法会将此 Symbol 注册到全局
isUniqueSymbol(Symbol.asyncIterator); // false 内置 Symbol 类型
isUniqueSymbol({}); // false 非 Symbol 类型

Policy Maps and Sets

提案链接: proposal-policy-map-set [7]

缓存在编程实践中一直是一个重要的领域,前端开发者和它打交道的次数更是数不胜数:DNS缓存、HTTP缓存、CDN缓存、本地缓存、服务器缓存等等。在 npm 社区,你也能找到许多用于缓存设计的工具包,如基于 LRU 策略的 lru-cache [8]quick-lru [9] 等。

此提案尝试为 JavaScript 中引入原生的缓存策略实现,包括 LRU (Least Recently Used,最近最少使用)、LFU(Least Frequently Used,最不常用)、FIFO(First In First Out,先进先出)与 LIFO (Last In First Out,后进先出),它们被实现为内置数据结构的形式:

new FIFOMap(maxNumOfEntries, entries = [])
new FIFOSet(maxNumOfValues, values = [])

new LIFOMap(maxNumOfEntries, entries = [])
new LIFOSet(maxNumOfValues, values = [])

new LRUMap(maxNumOfEntries, entries = [])
new LRUSet(maxNumOfValues, values = [])

new LFUMap(maxNumOfEntries, entries = [])
new LFUSet(maxNumOfValues, values = [])

这些结构基本实现了 Map 与 Set 上的方法(但它们并不是 Map 与 Set 的子类型),你也可以通过这些构造函数的 maxNumOfEntries / maxNumOfValues 来控制这些缓存结构的可用内存。

Function Memoization

提案链接: proposal-function-memo [10]

函数缓存指的是,对于一个函数建立起入参-结果的缓存表,在函数被使用某一新的入参调用时的返回值缓存起来,并在后续再次使用这一入参时直接返回此缓存值,而不会实际调用函数逻辑。

对于存在较大开销的计算过程,以及从状态到 UI 组件的计算这种场景,函数缓存会是非常好的优化手段,同时也可以基于其更好地实现单例模式(如确保对对象返回的是同一个引用)。

目前此提案提出的方式是新增 Function.prototype.memo 方法,也就是说对一个函数调用 memo 方法后,将返回它的缓存版本:

function f (x) { console.log(x); return x * 2; }

const fMemo = f.memo();

fMemo(3); // 打印 3,返回 6
fMemo(3); // 直接返回 6
fMemo(2); // 打印 2,返回 4
fMemo(2); // 直接返回 4
fMemo(3); // 直接返回 6

为了更简单地获取函数的缓存版本,此提案提出同时新增 @Function.memo 装饰器,来直接将一个函数标记为缓存版本(将无法再访问原版本):

@Function.memo
function f (x) { console.log(x); return x * 2; }

另外,此提案也希望将缓存表的控制也暴露出去,也就是说你可以自己传入一个实现了 .get() .has() .set() .get() 方法的类 Map 结构,来作为函数的缓存控制,上面提到的 Policy Maps and Sets 提案在这里就大有可为。

Object pick/omit

提案链接: proposal-object-pick-or-omit [11]

此提案将引入两个 Object 对象上的顶级方法:Object.pick 与 Object.omit,它们的作用正如其名,pick 将提取对象中的特定部分,而 omit 将移除对象中的特定部分。如果你使用过 Lodash 的 pick 和 omit 方法,那么应该对这两种操作非常熟悉。

目前在 JavaScript 中,我们可以通过解构赋值的方式来实现类 omit 的操作:

// 移除 obj 的 name、age 属性后得到 rest
const { name, age, ...rest } = obj;

但问题在于,如果我们想要移除的键名是动态的,那么这一方式就完全失效了,同时也无法基于解构赋值实现类 pick 的操作(pick 应当是基于子集进行处理,而非反过来基于差集)。另外,解构赋值并不能对原型对象上的属性进行处理。

使用这两个方法,我们可以进行更加符合直觉的对象操作了:

Object.pick(obj, ['job', 'sex']);
Object.omit(obj, ['name', 'age']);

除了基于键名来进行操作,这两个方法也支持使用一个 predictedFunction 函数来进行基于键值的判断,在此条件中返回 true 的属性将对应的被保留/移除:

Object.pick({a : 1, b : 2}, v => v === 1); // => { a: 1 }

这一使用方式类似于 Lodash 中的 pickBy / omitBy 方法。

结语

由贺师俊牵头,阿里巴巴前端标准化小组等多方参与组建的 JavaScript 中文兴趣小组(JSCIG,JavaScript Chinese Interest Group)在 GitHub 上开放讨论各种 ECMAScript 的问题,非常欢迎有兴趣的同学参与讨论:https://github.com/JSCIG/es-discuss/discussions 。

参考资料

[1]

tc39/test262: https://github.com/tc39/test262

[2]

tc39/ecma262: https://github.com/tc39/ecma262

[3]

proposal-hashbang: https://github.com/tc39/proposal-hashbang

[4]

proposal-duplicate-named-capturing-groups: https://github.com/tc39/proposal-duplicate-named-capturing-groups

[5]

proposal-import-reflection: https://github.com/tc39/proposal-import-reflection

[6]

proposal-symbol-predicates: https://github.com/rricard/proposal-symbol-predicates

[7]

proposal-policy-map-set: https://github.com/tc39/proposal-policy-map-set

[8]

lru-cache: https://www.npmjs.com/package/lru-cache

[9]

quick-lru: https://www.npmjs.com/package/quick-lru

[10]

proposal-function-memo: https://github.com/js-choi/proposal-function-memo

[11]

proposal-object-pick-or-omit: https://github.com/tc39/proposal-object-pick-or-omit