作者 | 吴成忠(昭朗)
这次 TC39 会议是 2020 年度最后一次全员会议。这次会议中没有任何提案争取到从 Stage 2 进入 Stage 3 的共识,也没有提案从 Stage 3 进入 Stage 4。
Stage 1 → Stage 2
从 Stage 1 进入到 Stage 2 需要完成撰写包含提案所有内容的标准文本的初稿。
Error Cause
提案链接:https://github.com/tc39/proposal-error-cause
提案文本链接:https://tc39.es/proposal-error-cause/
这个提案为 Error Constructor 新增了一个可选的参数 cause
,可以接受任意 JavaScript 值(JavaScript 可以 throw 任意值),并会把这个值赋值到 cause
属性上。
错误原因的特性在许多其他语言中都有类似的设计,如 C# Exception Cause,Java Exception Cause,Python raise exception from cause。同样的,在庞大的 JavaScript 生态中也已经有了非常广泛的使用,如 verror 每周有上千万的下载量,@netflix/nerror 每周有数十万的下载量。
不过,即使在 JavaScript 第三方库中有再多的使用,Chrome DevTools 等开发者工具也难以依赖这些第三方库中定义的、不是语言定义中存在的属性。有了这个提案之后,这些开发者工具也可以默认打印 cause
属性中的值了,可以为异常处理带来更良好的体验。
try {
return await fetch('//unintelligible-url-a')
.catch(err => {
throw new Error('Download raw resource failed', err)
})
} catch (err) {
console.log(err)
console.log('Caused by', err.cause)
// Error: Upload job result failed
// Caused by TypeError: Failed to fetch
}
Stage 0 → Stage 1
从 Stage 0 进入到 Stage 1 有以下门槛:
- 找到一个 TC39 成员作为 champion 负责这个提案的演进;
- 明确提案需要解决的问题与需求和大致的解决方案;
- 有问题、解决方案的例子;
- 对 API 形式、关键算法、语义、实现风险等有讨论、分析。
Stage 1 的提案会有可预见的比较大的改动,以下列出的例子并不代表提案最终会是例子中的语法、语义。
JavaScript Module Blocks
提案链接:https://github.com/tc39/proposal-js-module-blocks
目前很多设备都有非常多的处理器核心,JavaScript 代码在多个执行单元(线程、进程,如 Web Worker,Node.js 进程,Node.js worker_threads
)中同时执行也是越来越常见。但是现在的 JavaScript 对于多个执行单元间共享 JavaScript 代码并没有非常理想的方案,通常我们可以
1、将不同执行单元间的代码写在不同的文件里:
const Worker = new Worker('./my-worker.js')
worker.postMessage({ action: 'add', params: [40, 2] })
worker.addEventListener('message', data => alert(`Result: ${data}`))
2、通过一个通用的 Worker 执行器,然后将期望执行的 JavaScript 直接以字符串形式发送过去并执行(即每一次执行 JavaScript 引擎都需要重新解析这段 JavaScript 代码):
const result = await runInWorker('return "foobar"')
3、通过一个通用的 Worker 执行器,接受一个函数并将这个函数 toString 后直接以字符串发送执行:
function add(lhs, rhs) {
// 这里捕获了外部变量的话,难以检测
return lhs + rhs;
}
const result = await runInWorker(add, 40, 2)
// 不支持 async function
async function generateGraph() {
/** complex code */
}
这些方式要么不够工效,要么效率较差、更有安全风险。而这个提案则提出了一个隔离了变量作用域的代码块,并且这个代码块中的代码可以实现解析一次,到处使用。也意味着代码块中有任何语法问题,都可以快速地在引擎第一次解析即可被检查出来:
let workerCode = module {
onmessage = function({ data }) {
let mod = await import(data);
postMessage(mod.fn());
};
};
let worker = new Worker(workerCode, { type: 'module' });
worker.onmessage = ({ data }) => alert(data);
worker.postMessage(module { export function fn() { return 'hello!' } });
其实在之前 TC39 已经有过类似的提案 Blöck。不过之前这个提案的主要推进者已经退出了 TC39,提案也长期未有更新,故现在有新的委员会代表继续拾起想法开始推进。
Extensions
提案链接:https://github.com/hax/proposal-extensions
在这个提案之前,TC39 已经有过多个相关的提案,如 Pipeline Operator (Stage 1) ,Bind Operator (Stage 0)。其中 Bind Operator 的主要 Champion Group 成员已经长期没有推动提案的演进,而这次这个 Extensions 提案即是重新拾起了 Bind Operator 的大部分想法,并作了部分设计上的修订,继续提案的推进。
目前提案在技术委员会上的演示内容包含了几个部分的动机:
- 为任意 JavaScript 值引入局部作用域的方法与访问器(Accessor)扩展,替代 Monkey Patch 成为 JavaScript 中具有高工效、隔离性的扩展机制;
- 为扩展声明定义一个独立的命名空间,避免扩展声明被普通变量声明覆盖。
提案的内容可以简单地总结为以下几个例子:
// 为集合类型拓展 toSet 的方法定义
const ::toSet = function () { return new Set(this) }
// 给 DOM 对象拓展 allDivs 属性访问器
const ::allDivs = function {
get() { return this.querySelectorAll('div') }
}
const ::flatMap = Array.prototype.flatMap
const ::size = Object.getOwnPropertyDescriptor(Set.prototype, 'size')
let classCount = document
::allDivs
::flatMap(element => element.classList)
::toSet()
::size
除了直接在局部作用域声明扩展之外,还可以从外部模块导入扩展声明,或者是使用 namespaceOrConstructor:method
来使用快捷访问一个扩展:
import ::{ identity } from '某些模块'
arrayLike
::Array:map(x => x) // Array.prototype.map.call(arrayLike, x => x)
::identity() // identity.call(arrayLike)
相对于之前的 Bind Operator 提案来说,目前的 Extensions 提案有以下不同之处:
- 增加扩展字段访问器的定义;
- 对于局部定义的扩展方法、字段访问器有独立的变量命名空间;
- 增加使用命名空间扩展(obj::namespace:extension)的语法;
- :: 操作符现在与 . 操作符的优先级相同;
- 移除了 ::obj.foo (BindExpression),这个表达式相当于是 foo.bind(obj) 的语法糖。
相比于 Pipeline Operator 的将运算符的左运算符作为右运算符的第一个参数,这个提案中的 :: 运算符是将左运算符作为右运算符的 this 参数。不过,通过这个提案其实也能在一定程度上获得类似的体验:
function capitalize (str) {
return str[0].toUpperCase() + str.substring(1);
}
function exclaim (str) {
return str + '!'
}
/**
* Pipeline Operator 例子
*/
let result = "hello"
|> capitalize
|> exclaim
result // => 'Hello!'
/**
* 扩展例子
*/
const ::pipe = function (fn) { return fn(this) }
let result = "hello"
::pipe(capitalize)
::pipe(exclaim)
result // => 'Hello!'
值得注意的是,当前 Pipeline Operator 的一个未完成的设计就是不支持 async/await,而对于扩展语法来说,这就有比较大的可自定义空间:
async function post(data) {
const resp = await fetch('127.0.0.1', { method: 'POST', body: data })
return resp.text()
}
/**
* Pipeline Operator 例子
*/
let result = "127.0.0.1:8080"
|> await post // 语法错误
/**
* 扩展例子
*/
const ::pipeAwait = async function (fn) { return fn(await this) }
let result = await "hello"
::pipeAwait(post)
::pipeAwait(post)
另外,通过扩展语法我们更可以实现类似自定义单位的方案:
const ::px = Extension.accessor(CSSUnitValue.px) // 非现有 API
1::px // CSSUnitValue {value: 1, unit: "px"}
除了以上所述的使用场景之外,目前提案给予了扩展的声明独立命名空间的设计上在会议上有较多的异议。前有例子如 Decorator 的第3个版本,它同样具有类似的独立命名空间设计,在委员会上存在强烈的异议。所以虽然提案目前总体进入了 Stage 1,相信这个设计后续会有更多针对性的讨论。
Grouped Accessor
提案链接:https://github.com/rbuckton/proposal-grouped-and-auto-accessors
这是一个来自微软的代表的提案,提案主要动机是让 JavaScript 的字段访问器(getter、setter)可以写在一起,同时给 Decorator 提案设计带来一个更加工效的字段装饰方案:
class C {
x {
get() { ... } // equivalent to `get x() { ... }`
set(value) { ... } // equivalent to `set x(value) { ... }`
}
}
const obj = {
x {
get() { ... }
set() { ... }
}
}
class D {
@logger() // 同时装饰 getter/setter
x {
get() { ... }
set(value) { ... }
}
}
不过以目前 JavaScript 现有的字段访问器声明语法,较难让人信服新的方案能够带来更好的语法一致性。如果和现有的 getter/setter 与新的语法一起使用,语义上并没有增加新的特性,反而却额外增加了一个写法,可能会带来更多问题。这个提案在本次会议上也是在非常多的讨论后才进入 Stage 1。
总结
@JackWorks :如果现在所有的 TC39 提案都写在一起 JavaScript 会变成什么样?
每年进入 TC39 的提案多种多样,而 TC39 的分阶段的流程必然给这些提案与提案的推进者们施加了强大的推进阻力。不过也正是这些分阶段性的推进阻力,让推进者们必须阐明提案所希望解决的问题,提案如何解决问题,提案能否解决问题,是否应该成为 ECMAScript 的一部分等等灵魂发问,才不会让 ECMAScript 变成一个揉杂了各种突发奇想的产物。
???? 第十五届 D2 前端技术论坛 语言框架专场
TC39 核心成员 Ujjwal Sharma 将带来主题分享 《揭秘TC39:ES2020和ES2021》。重点讨论ES2020和ES2021中令人兴奋的新特性,并讨论我们在 TC39中正在进行的一些提案,包括Temporal和Intl.DurationFormat,同时与大家分享我在这个过程中学习到的一些经验,以及如何将它们应用在自己的工作中。最后为大家介绍 TC39 是如何工作的,以及社区的同学如何参与 TC39 并发表自己的意见。
关注「Alibaba F2E」
把握阿里巴巴前端新动向