2022 年 7 月 19 日至 21 日,由 Google 主办的 TC39 第 91 次会议在美国旧金山举行。以下 ECMAscript 提案在会议上取得了阶段性进展:
☀️ 第四阶段:
- Hashbang Grammar:允许 JavaScript 文件头使用
#!
用于 shell 识别解释器。
🌟 第三阶段:
- Duplicate named capturing groups:允许正则表达式捕获组的命名重复。
💫 第二阶段:
- Import Reflection:建议使用导入反射属性导入 ES 模块的语法。
🌙 第一阶段:
- Symbol Predicates:建议引入区分 symbol 的方法。
- Policy Maps and Sets:具有缓存替换策略(如 LRU 和 LFU)的 Maps 和 Sets 提案。
- Function Memoization:函数记忆。
- Object pick/omit:符合人体工程学的动态对象重构。
对于新提案,从提出到最后被纳入 ECMAScript 新特性,TC39 的规范中分为五步:
- stage0(strawman),任何TC39的成员都可以提交。
- stage1(proposal),进入此阶段就意味着这一提案被认为是正式的了,需要对此提案的场景与API进行详尽的描述。
- stage2(draft),演进到这一阶段的提案如果能最终进入到标准,那么在之后的阶段都不会有太大的变化,因为理论上只接受增量修改。
- state3(candidate),这一阶段的提案只有在遇到了重大问题才会修改,规范文档需要被全面的完成。
- state4(finished),这一阶段的提案将会被纳入到ES每年发布的规范之中。
1、第四阶段
(1)Hashbang Grammar
Unix 的命令行脚本都支持#!
命令,又称为 Hashbang。这个命令放在脚本的第一行,用来指定脚本的执行器。Hashbang Grammar 提案就是想为JavaScript 脚本引入了#!命令,这个命令写在脚本文件或者模块文件的第一行:
// 写在脚本文件的第一行
#!/usr/bin/env node
'use strict';
console.log(1);
// 写在模块文件的第一行
#!/usr/bin/env node
export {};
console.log(1);
这样,Unix 命令行就可以直接执行脚本了:
# 以前执行脚本
node hello.js
# 有了 hashbang 之后执行脚本
./hello.js
不过这样的话,hashbang 就必须严格的在文件头,否则就会出现语法错误,导致这个JavaScript脚本文件无法使用。
提案地址:https://github.com/tc39/proposal-hashbang
2、第三阶段
(1)Duplicate named capturing groups
在正则表达式中,可以使用捕获组来对匹配模式中的某一部分做独立匹配。现在,在 JavaScript 中,正则表达式中的命名捕获组需要是唯一的。
const str = "2022-07";
const reg = /(?<year>[0-9]{4})-(?<month>[0-9]{2})/;
const group = str.match(reg).groups;
group.year; // '2022'
对于以下正则表达式,即匹配“2022-07”格式和“07-2022”格式:
/(?<year>[0-9]{4})-[0-9]{2}|[0-9]{2}-(?<year>[0-9]{4})/
这是一个错误,因为重复使用了名称year
。但有时想匹配一个可以用多种格式编写的东西(如上)。能够在这种情况下使用相同的命名会很好。Duplicate named capturing groups 提案就是为解决这个问题。此提案提出允许捕获组的命名可以重复,以此来支持上面这种场景。此提案允许在名称出现在不同|
时重用它们。
提案地址:https://github.com/tc39/proposal-duplicate-named-capturing-groups
3、第二阶段
(1)Import Reflection
对于 JavaScript 和 WebAssembly,需要能够在标准主机执行模型之外更紧密地自定义模块的加载、链接和执行。对于 JavaScript,创建 userland loaders 需要模块反射类型,以便共享主机解析、执行、安全和缓存语义。
在语法上支持模块反射作为一种新的导入形式创建了一个原语,可以将模块的静态、安全和工具优势从 ESM 集成扩展到这些动态实例化用例中。
import module x from "<specifier>";
module
反射类型被添加到 ImportStatement
的开头。仅支持上述形式,不支持命名导出和未绑定声明。
动态导入:
const x = await import("<specifier>", { reflect: "module" });
对于动态导入,模块导入反射在指定导入断言的第二个属性选项包中使用 reflect 指定。
提案地址:https://github.com/tc39/proposal-import-reflection
4、第一阶段
(1)Symbol Predicates
该提案提出了两个区分 symbol 的方法:
- Symbol.isRegistered(symbol)
- Symbol.isWellKnown(symbol)
在实际使用时,并非所有 symbol 都是相同的,并且更多地了解它们的含义可能很有用,尤其是对于库而言。了解 symbol 是否真正独一无二、可伪造、跨领域共享可能很重要,具体还是要器具与其用例。
可以在库中检测 symbol 是否可以用作 WeakMap 键:
function isWeakMapKey(key) {
switch (typeof key): {
case "object":
return key !== null;
case "function":
return true;
case "symbol":
return !Symbol.isRegistered(sym);
}
return false;
}
isWeakMapKey({}); // true
isWeakMapKey(Symbol()); // true
isWeakMapKey("foo"); // false
isWeakMapKey(Symbol.for("foo")); // false
isWeakMapKey(Symbol.asyncIterator); // true
了解是否获得了真正独一无二的 symbol:
const isUniqueSymbol = sym => typeof sym === "symbol" && !(Symbol.isRegistered(sym) || Symbol.isWellKnown(sym));
isUniqueSymbol(Symbol()); // true
isUniqueSymbol(Symbol.for("foo")); // false
isUniqueSymbol(Symbol.asyncIterator); // false
isUniqueSymbol({}); // false
提案中的两个方法:
Symbol.isRegistered(value)
:将未知值作为唯一参数,返回布尔值:如果 symbol 已注册,则返回 true,否则返回 false。Symbol.isWellKnown(value)
:将未知值作为唯一参数,返回布尔值:如果 symbol 是 ECMA262 和 ECMA402 定义的已知的 symbol 之一,则返回 true,否则返回 false。
两个方法的 Polyfill:
Symbol.isRegistered(symbol)
: https://github.com/inspect-js/is-registered-symbolSymbol.isWellKnown(symbol)
: https://github.com/inspect-js/is-well-known-symbol
提案地址:https://github.com/tc39/proposal-symbol-predicates
(2)Policy Maps and Sets
开发人员经常使用 Map 数据结构作为缓存,并且有时希望限制缓存的内存消耗。建议探索向 JavaScript 语言添加映射数据结构,以支持各种基本的简单缓存替换策略,例如 LRU(最近最少使用)、LFU(最不常用)、FIFO(先进先出)和 LIFO (后进先出)。
该提案将向全局对象添加几个内置类。它们中的每一个都有一个可变的类似 Map 的接口或一个可变的类似 Set 的接口。
对于类似 Map 的类,这些包括:
m.size
: m 中的值的数量。m.has(key)
: 返回一个布尔值,m 是否具有给定 key。m.get(key)
:查找指定的 key,如果有则返回 m 中=该 key 对应的的 value,否则返回undefined。m.set(key, value)
:将 m 中key 的值设置为 value,返回m自身。m.delete(key)
:删除指定 key 及对应的 value(如果有)。返回一个布尔值,表示在删除之前 m 中是否有该 key。m.clear()
:从 m 中删除所有内容。返回 undefined。m[Symbol.iterator]():
不确定是否应该实现。m.entries()
: 不确定是否应该实现。m.keys():
不确定是否应该实现。m.values()
: 不确定是否应该实现。m.forEach()
: 不确定是否应该实现。
对于类似 Set 的类,这些包括:
s.size
:s中值的个数。s.has(value)
:返回 s 是否具有给定 value 的布尔值。s.add(value)
:将给定值添加到 s,返回 s 本身。s.delete(key)
:如果 s 有值,则从 s 中删除给定值。返回在删除值之前 s 是否具有值的布尔值。s.clear()
:从 s 中删除所有值,返回 undefined。m[Symbol.iterator]()
:不确定是否应该实现。s.values()
:不确定是否应该实现。s.forEach()
:不确定是否应该实现。
(1)FIFOMap 和 FIFOSet
new FIFOMap(maxNumOfEntries, entries = [])
new FIFOSet(maxNumOfValues, values = [])
如果给定非整数最大数量的 entries/values
,或者初始 entries/values
不可迭代,则构造函数将抛出 TypeErrors。
它们的实例按照它们添加的顺序逐出 entries/values
,就像 FIFO 队列一样。
(2)LIFOMap and LIFOSet
new LIFOMap(maxNumOfEntries, entries = [])
new LIFOSet(maxNumOfValues, values = [])
如果为这些构造函数提供了非整数的最大 entries/values
,或者初始 entries/values
不可迭代,则这些构造函数将抛出 TypeErrors。
它们的实例按照添加顺序逐出 entries/values
,就像 LIFO 堆栈一样。
(3)LRUMap and LRUSet
new LIFOMap(maxNumOfEntries, entries = [])
new LIFOSet(maxNumOfValues, values = [])
如果为这些构造函数提供了非整数的最大 entries/values
数,或者初始 entries/values
不可迭代,则这些构造函数将抛出 TypeErrors。
(4)LFUMap 和 LFUSet
new LIFOMap(maxNumOfEntries, entries = [])
new LIFOSet(maxNumOfValues, values = [])
如果为这些构造函数提供了非整数的最大 entries/values
数,或者初始 entries/values
不可迭代,则这些构造函数将抛出 TypeErrors。
替代解决方案
可以向现有的 Map 和 Set 构造函数添加可选参数:
const cache = new Map(initialEntries, 256, policyType);
提案地址:https://github.com/tc39/proposal-policy-map-set
(3)Function Memoization
函数记忆是一种常用的技术,它会缓存函数调用的结果,并在再次出现相同的输入时返回缓存的结果。这些对以下情况会有用:
- 在时空权衡中优化昂贵的函数调用(例如,阶乘、斐波那契数);
- 缓存状态→UI 计算(例如,在 React 的 useMemo 中);
- 确保回调始终返回相同的单例对象;
- 逻辑编程中的表格;
函数记忆时很有用的。建议探索在 JavaScript 语言中添加一个 memoization API。
Function.prototype.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
。 这将更容易将 memoization
应用于函数声明:
@Function.memo
function f (x) { console.log(x); return x * 2; }
任何一个版本都可以使用递归函数:
// 原型方法版本
const getFibonacci = (function (n) {
if (n < 2) {
return n;
} else {
return getFibonacci(n - 1) +
getFibonacci(n - 2);
}
}).memo();
console.log(getFibonacci(100));
// 函数装饰器版本
@Function.memo
function getFibonacci (n) {
if (n < 2) {
return n;
} else {
return getFibonacci(n - 1) +
getFibonacci(n - 2);
}
}
console.log(getFibonacci(100));
结果缓存实现
开发人员将能够传递一个可选的 cache
参数,此参数必须是具有 .has
、.get
和 .set
方法的类 Map 对象。
(1)元组 keys?
我们可以使用元组作为缓存的 key。每个元组代表对记忆函数的函数调用,元组的格式为#[thisVal, newTargetVal, ...args]
。
对象值将替换为唯一标识该对象的 symbol。(元组不能直接包含对象。memoized 函数的闭包将关闭一个内部 WeakMap,该 WeakMap 将对象映射到它们的 symbol。)
const cache = new LRUMap(256);
const f = (function f (arg0) { return this.x + arg0; }).memo(cache);
const o0 = { x: 'a' }, o1 = { x: 'b' };
f.call(o0, 0); // 返回 'a0'
f.call(o1, 1); // 返回 'b1'
现在缓存是 LRUMap(2) { #[s0, undefined, 0] ⇒ 'a0', #[s1, undefined, 1] ⇒ 'b1' }
,其中 s0 和 s1 是唯一的 symbol。 f 的闭包会在内部关闭 WeakMap { o0 ⇒ s0, o1 ⇒ s1 }
。
memo 的默认行为(即不提供缓存参数)是不确定的。 它可能只是一个无界的普通 Map。 (WeakMaps 不能包含元组作为它们的 key)。
(2)组合 key?
缓存 key 的另一种选择是组合 key。 每个组合 key 代表对记忆函数的函数调用,组合 key 的形式为 CompositeKey(thisVal, newTargetVal, ...args)
。
const cache = new LRUMap(256);
const f = (function f (arg0) { return this.x + arg0; }).memo(cache);
const o0 = { x: 'a' }, o1 = { x: 'b' };
f.call(o0, 0); // 返回 'a0'
f.call(o1, 1); // 返回 'b1'
现在缓存是 LRUMap(2) { compositeKey(o0, undefined, 0) ⇒ 'a0', compositeKey(o1, undefined, 1) ⇒ 'b1' }
。
memo 的默认行为(即不提供缓存参数)是不确定的。 它可能只是一个 WeakMap,它能够包含组合 key 作为它们的 key。
提案地址:https://github.com/tc39/proposal-function-memo
(4)Object pick/omit
该提案旨在实现 Object.pick
,Object.omit
方法,类似于 TypeScript 中的 Pick 和 Omit 工具函数。
先来看看这两个方法试图解决什么问题:
- 在 MouseEvent 上,只需要 ‘ctrlKey’、’shiftKey’、’altKey’、’metaKey’ 事件
- 有一个 configObject,只需要它的 [‘dependencies’, ‘devDependencies’, ‘peerDependencies’]
- 从 req.body 中提取 [‘name’, ‘company’, ‘email’, ‘password’]
- 有一个 depsObject,需要忽略其中的所有 @internal/packages
- 需要通过从 ({ …state.models, …action.update }) 中删除 action.deleted 来构造一个 newModelData
从这些例子中可以看出来,对于一个对象,很多时候我们只想要其中的一部分属性或者不想要其中的一些属性。如果 JavaScript 对象提供了 Object.pick
,Object.omit
方法,就可以轻松解决上述问题。
有人认为,我们可以实现如下 pick
和 omit
:
const pick = (obj, keys) => Object.fromEntries(
keys.map(k => obj.hasOwnProperty(k) && [k, obj[k]]).filter(x => x)
);
const omit = (obj, keys) => Object.fromEntries(
keys.map(k => !obj.hasOwnProperty(k) && [k, obj[k]]).filter(x => x)
);
这样的实现存在以下问题:
- 不符合人体工程学;
- 如果选择解构的方式,它不适用于 pick,或者对于动态值的省略;
- 解构不能克隆新对象,而 Object.pick 可以;
- 解构不能从原型中获取属性,而 Object.pick 可以;
- 解构不能动态选择属性,而 Object.pick 可以;
- 解构不能省略一些属性,没有这个提议我们只能克隆和删除。
语法:
Object.pick(obj[, pickedKeys | predictedFunction(currentValue[, key[, object]])[, thisArg])
Object.omit(obj[, omittedKeys | predictedFunction(currentValue[, key[, object]])[, thisArg])
使用方式:
// default
Object.pick({a : 1}); // => {}
Object.omit({a : 1}); // => {a: 1}
Object.pick({a : 0, b : 1}, v => v); // => {b: 1}
Object.pick({a : 0, b : 1}, v => !v); // => {a: 0}
Object.pick({}, function () { console.log(this) }); // => 对象本身
Object.pick({}, function () { console.log(this) }, window); // => Window
Object.pick({a : 1, b : 2}, ['a']); // => {a: 1}
Object.omit({a : 1, b : 2}, ['b']); // => {a: 1}
Object.pick({a : 1, b : 2}, ['c']); // => {}
Object.omit({a : 1, b : 2}, ['c']); // => {a: 1, b: 2}
Object.pick([], [Symbol.iterator]); // => {Symbol(Symbol.iterator): f}
Object.pick([], ['length']); // => {length: 0}
Object.pick({a : 1, b : 2}, v => v === 1); // => {a: 1}
Object.pick({a : 1, b : 2}, v => v !== 2); // => {a: 1}
Object.pick({a : 1, b : 2}, (v, k) => k === 'a'); // => {a: 1}
Object.pick({a : 1, b : 2}, (v, k) => k !== 'b'); // => {a: 1}