ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JavaScript提案机制与Stage 3新特性:从TC39演进到未来编码方式

JavaScript提案机制与Stage 3新特性:从TC39演进到未来编码方式 JavaScript 社区每隔一段时间就会冒出一批“新东西”而比新东西更早出现在你时间线上的往往是各种提案。做前端的人应该都感受过那种矛盾一边是生产环境里写着 ES2020 时代的老代码一边是 TC39 会议上刚讨论到一半、连语法糖都还没定下来的未来特性。这种“未来”和“现在”之间的拉扯恰恰是 JavaScript 生态最有意思的地方。这篇文章我想认真聊聊 JavaScript 的提案机制和近期值得关注的新特性。不是那种把 spec 搬运一遍的解读而是站在一个普通开发者的角度探讨哪些提案值得你提前了解、哪些正在改变我们的编码方式、哪些项目可能就诞生在某个提案的启发之下。内容适合所有 JavaScript 开发者无论你是在写 React、Vue还是做 Node.js 服务端理解提案的推进规律会让你对语言本身的演化更有掌控感。1. 提案机制怎么说从创意到标准的多级关口1.1 TC39 与 Stage 0-4 的完整链路ECMAScript 标准的演进并不是“某个大神提一个需求然后开会通过”那么简单。负责管理 JavaScript 标准的委员会叫 TC39它把每一个新特性从提出到落地划分成了五个阶段也就是大家常说的 Stage 0 到 Stage 4。每个阶段都是一道关口提案必须达到相应成熟度才有资格进入下一阶段。Stage 0strawman稻草人任何人都可以提交的想法可能只是几段粗糙的代码或一个简短的问题描述没有任何 API 设计甚至可能只是一个纯粹的“愿望”。Stage 1proposal提案TC39 正式采纳这个方向会有一个 champion负责人跟进描述问题域、用例、潜在语义和难点。进入这个阶段说明委员会认为它值得认真讨论。Stage 2draft草案提案有了初步的语法和 API 设计文本但细节还在变动。Stage 2 意味着委员会认可了整体方案开始逐条打磨。Stage 3candidate候选API 结构基本冻结不再接受破坏性修改重点转向收集实现反馈。这个阶段最值得开发者关注因为后面改动会很小你看到的基本就是最终形态。Stage 4finished定稿提案已经通过测试套件被多个实现支持正式写入规范。下一版 ECMAScript 发布时它就是标准的一部分。这个过程可以用一个粗糙的比喻来理解Stage 0 像是你朋友在饭局上随口说的“要不我们做个 XX 功能”Stage 1 是你决定认真立项Stage 2 出了第一版详细设计方案Stage 3 是图纸冻结、开始施工试运行Stage 4 则是竣工验收。很多前端工程师只盯着最后的结果却忽略了 Stage 2 到 Stage 3 之间才是影响语言走向的黄金讨论期。1.2 为什么建议关注 Stage 3 而非 Stage 4我的经验是真正值得开发者花时间研究的永远是 Stage 3而不是 Stage 4。Stage 4 的特性虽然已经进入标准但“进入标准”和“你代码里能用上”之间往往隔着 Node.js 版本、浏览器发布周期、构建工具版本好几道墙。相反Stage 3 的提案已经冻结了 API你完全可以通过 Babel 插件、Polyfill 或者 Nightly 版本的运行时提前体验。更重要的是Stage 3 阶段往往是讨论最充分、社区反馈最真实的时期。提案的 champion 需要收集各种实现方的意见如果你发现某个 API 设计在实际场景里很别扭现在提意见还来得及。等它变成 Stage 4再提意见基本就是石沉大海。我见过不少开发者抱怨“这个 API 设计太怪了”可他们根本没参与过 Stage 3 的反馈期这种抱怨其实挺没必要的。另外TC39 现在的节奏已经变成了每年发布一版 ECMAScript 标准也就是说 Stage 4 的提案通常会在一年内正式进入规范。到了 Stage 3你拿到的 API 和一年后正式发布的内容基本没有差别提前学习的时间成本很低收益却很明确。1.3 标准节奏背后的逻辑变化早期 ECMAScript 的版本更迭很慢ES5 到 ES6 隔了六年ES6 到 ES7 又用了将近一年半。后来 TC39 调整了策略把大型、复杂的特性拆成更小的独立提案每个提案各自走自己的阶段不再捆绑成大版本一次性发布。这种变化直接影响了我们接收新特性的方式过去你要等一个“大版本”才能吃到一堆新语法现在几乎每隔几个月就能在浏览器里看到一个新 API 落地。这个“小步快跑”的模式也改变了工具链的地位。以前 Babel 是“提前使用未来特性”的代名词现在仍然很重要但原生实现追赶的速度明显加快了——V8 和 SpiderMonkey 的团队会紧盯着 Stage 3 提案在提案冻结后很快就实现出来这给了开发者更多选择。2. 值得关注的提案近期最值得花时间的几个方向2.1 Decorators 装饰器五年长跑终于走到关键阶段如果你用过 Python 的装饰器、Java 的注解或者 C# 的 Attribute那么 JavaScript 的 decorators 提案对你来说绝不陌生。它本质上是一种在类、方法、属性和访问器上附加逻辑的语法可以让你在声明处直接包装、替换或配置目标对象。这个提案在 JavaScript 社区里折腾了至少五年光是语义就推倒重来了好几次目前处于 Stage 3。新版装饰器语义和早期 TypeScript 的 experimentalDecorators 有很大区别装饰器不再是简单的“函数叠加”而是通过addInitializer来注册初始化逻辑支持value/get/set等多种元数据装饰器只能应用于类、类方法、类访问器、类字段不能装饰普通函数或对象字面量静态块和私有元素也能被装饰适用范围比以前更广。举个简单例子一个memoize装饰器可能是这样function memoize(fn, context) { const cache new Map(); return function (...args) { const key JSON.stringify(args); if (cache.has(key)) return cache.get(key); const result fn.apply(this, args); cache.set(key, result); return result; }; } class Calculator { memoize factorial(n) { return n 1 ? 1 : n * this.factorial(n - 1); } }这里memoize接收原方法并返回一个带缓存的封装版本。在框架层面装饰器很适合做依赖注入、组件注册、权限标记、日志埋点这类横切关注点。实际开发中Angular 和 NestJS 已经重度使用装饰器只是它们目前还是基于 TypeScript 的 experimental 模式。标准装饰器一旦定稿并得到运行环境支持这些框架的元编程能力会有一次明显的跃升。从实操角度看我现在不会建议在大型生产项目里大规模用标准装饰器因为 Babel 的配置和 Stage 3 的语义细节都还需要磨合。但在规模可控的内部工具、小玩具项目里试试水是了解它最佳方式。2.2 TemporalDate 的“继承人”终于要来了JavaScript 的Date对象长期被吐槽月份从 0 开始时区处理困难API 设计老旧而且所有操作都是可变的——setMonth直接改原对象。如果你想做跨时区的日历计算原生Date基本是一场灾难。Temporal 提案的目标就是彻底替换Date在日历和时钟领域的位置提供一套更现代、不可变、支持多种日历体系的时间 API。目前处于 Stage 3而且实现已经比较完整。Temporal 的核心类型包括Temporal.PlainDate纯日期不含时间和时区Temporal.PlainTime纯时间不含日期和时区Temporal.PlainDateTime不含时区的日期和时间组合Temporal.Instant时间戳表示绝对时间点Temporal.ZonedDateTime带时区和日历的完整时间点Temporal.Duration时间段支持年、月、日、小时、分钟、秒、毫秒等粒度Temporal.Now获取当前时间点的各种便捷方法。Temporal 最大的亮点是不可变性和清晰的计算方法。比如你想给某天加一个月不需要手动处理每个月的天数差异const date Temporal.PlainDate.from(2024-01-31); const nextMonth date.add({ months: 1 }); console.log(nextMonth.toString()); // 2024-02-29同样的操作如果用原生Date你需要自己判断目标月份有多少天。Temporal 还解决了“混用时区”的问题ZonedDateTime明确包含时区和日历不会默默做本地时区转换。在实际项目里我建议在涉及排班、日历、订单周期计算的场景优先考虑引入js-temporal/polyfill来试用 Temporal。虽然它体积不小但对于时间逻辑复杂的模块它的可读性和正确性远胜自己手写日期计算。等未来原生支持普及再把 polyfill 换成原生实现即可。2.3 类型注解JavaScript 与 TypeScript 的一次世纪和解尝试这个提案比较特殊目前还处在 Stage 1由 TypeScript 团队提出目标是给 JavaScript 增加类型注解语法但把类型检查完全排除在运行时之外。通俗理解你可以在 JS 文件里写let count: number 42JS 引擎会像忽略注释一样忽略: number这部分类型检查交给外部的类型检查器比如 tsc 或 type checker完成。这种设计一旦落地意味着我们可能不再需要构建步骤来剥离类型也用不着 TypeScript 编译器生成.js文件。Node.js 已经有实验性的--experimental-strip-types功能Web 浏览器也可以直接运行带类型注解的 JS。它为什么会引起这么大关注因为它直接关系到一个核心问题JavaScript 生态到底是“继续依靠 TypeScript 做类型层”还是“语言本身就原生支持类型语法”。当然这个提案要落地还有很长的路要走。类型注解只是语法层面的“外壳”TypeScript 的核心价值在于类型系统和编译器对类型的深入分析。就算 JS 原生支持了类型注解TS 的 utility types、泛型约束、条件类型等也不会原封不动搬到 ECMAScript 规范里。我个人的判断是未来两到三年我们会看到一个“JS 语法支持类型注解TS 专注类型算法边界越来越清晰”的格局。从这个提案里能看出的趋势是TC39 越来越重视开发者体验和生态兼容性而不是像早期那样只关心语言理论。提案的 champion 会和 TypeScript 团队保持密切沟通尽量确保新语法不会破坏现有的 TS 生态。2.4 正则、异步迭代与底层 API 的小步进化除了那些“改变世界”的大提案TC39 还在推进一批小而有用的 API它们单看不显眼放在一起却能显著提升开发效率。RegExp.escape提案Stage 3解决了一个常见痛点我们需要把用户输入转义成正则字符串时经常要手写/[.*?^${}()|[\]\\]/g。这个提案直接提供RegExp.escape(str)语义清晰不再容易写错。Array.fromAsync已经进入 Stage 4。它跟Array.from类似但从异步可迭代对象、Promise 数组等来源创建数组并且对每个元素执行await。它在需要并发拉取多个异步数据源、然后统一处理结果的场景下非常顺手。Promise.withResolvers也是已经进入 Stage 4 的提案。以前要拿到一个 Promise 的 resolve 和 reject 函数必须写一个比较绕的包装器现在可以这样const { promise, resolve, reject } Promise.withResolvers();还有Uint8Array.prototype.toBase64()和fromBase64()这个提案一旦落地编码解码就会变得简单很多const bytes new Uint8Array([72, 101, 108, 108, 111]); const b64 bytes.toBase64(); console.log(b64); // SGVsbG8这类小提案的价值在于降低“随手写工具函数”的心智负担。很多时候我们写代码感到繁琐不是逻辑复杂而是基础 API 不够顺手。这些渐进式改进虽然不会成为技术新闻的头条却会在几年的时间里默默改变你的日常编码体验。2.5 更远期的探索信号量、调度器与结构化克隆如果把目光再放远一点TC39 也在探索并发和调度相关的方向。scheduler提案希望给 JavaScript 提供一种更精细的任务调度机制能区分用户交互优先级和后台任务优先级。想想现在的前端应用大量图片加载、数据分析任务和用户点击事件挤在同一个事件循环里全靠手写requestIdleCallback或setTimeout来“偷时间”这显然不够优雅。Scheduler如果正式进入标准未来我们可能可以像这样表达优先级scheduler.postTask(() { // 高优先级任务 }, { priority: user-blocking });这类提案虽然短期内无法落地但方向已经明确了JavaScript 正在从一个“单线程事件驱动”的语言逐渐走向“显式表达并发度和优先级”的平台级语言。这意味着前端不仅能写出更复杂的交互还能有效治理渲染性能和后台任务之间的冲突。我更想强调的是并发和并行是两个不同的问题。Worker、SharedArrayBuffer 解决的是“并行”和“共享内存”而 Scheduler、Promise 机制解决的是“并发调度”。未来两者会进一步结合让数据密集型应用在浏览器里获得更接近原生应用的性能体验。3. 实际开发中怎么尝鲜配置、工具与直接能用的代码3.1 用 Babel 提前跑起来如果你想在项目里体验 Stage 3 的语法提案Babel 几乎是最成熟的选择。以装饰器为例你需要装这几个包npm install --save-dev babel/core babel/cli babel/preset-env babel/plugin-proposal-decorators然后在 Babel 配置里显式声明装饰器的版本语义{ plugins: [ [babel/plugin-proposal-decorators, { version: 2023-11 }] ], presets: [ [babel/preset-env, { targets: { node: current } }] ] }这里最大的坑是版本参数。老版本 Babel 默认走的是legacy语义也就是 TypeScript 的 experimentalDecorators 那套。如果你直接用默认配置却写了新式装饰器的代码会在编译阶段遇到各种奇怪报错。所以一定要显式指定version: 2023-11。我踩过一次这个坑当时把代码交给同事他那边跑不动最后发现是 Babel 的装饰器版本配置不一致。3.2 通过 Polyfill 使用 Temporal对于 Temporal 这类纯 API 提案不需要语法转译只要引入 Polyfill 就能在现有环境里跑。官方推出了js-temporal/polyfill包用法很直接npm install js-temporal/polyfillconst { Temporal } require(js-temporal/polyfill); const now Temporal.Now.zonedDateTimeISO(); console.log(now.toString()); const duration Temporal.Duration.from({ hours: 1, minutes: 30 }); console.log(duration.total(minutes)); // 90在浏览器端你可以直接通过 CDN 引入 UMD 包或使用 ES Module 方式导入。需要注意Temporal 的 API 数量比较多Polyfill 体积也不小建议只在确实需要的时间处理模块里局部引入不要全局扩散。而且 Temporal 提案的某些边界语义还在微调虽然 API 结构已经冻结但如果你做的是对时间精度要求极其苛刻的金融类应用现阶段还是保守一点先用成熟库如 dayjs 或者 luxon 更稳。3.3 原生环境中直接可用的 Stage 4 新特性除了通过工具链提前体验很多 Stage 4 特性已经在现代运行时里原生可用了。拿Array.fromAsync举例我在 Node.js 20 或最新浏览器里可以直接跑async function* asyncSequence() { for (let i 0; i 5; i) { await new Promise((resolve) setTimeout(resolve, 10 * i)); yield i * 2; } } const results await Array.fromAsync(asyncSequence()); console.log(results); // [0, 2, 4, 6, 8]再比如Promise.withResolvers写异步初始化逻辑时会舒服不少function waitForEvent(target, eventName) { const { promise, resolve, reject } Promise.withResolvers(); target.addEventListener(eventName, resolve, { once: true }); target.addEventListener(error, reject, { once: true }); return promise; }这类新 API 的意义不在于把代码写得多么花哨而在于减少样板代码。你不需要每次手动 new Promise 再手动把 resolve 函数丢到事件回调里了。3.4 使用工具查提案状态一条建议路径尝鲜之前先搞清楚提案到底处于哪个阶段非常关键。我常用的几个渠道官方提案列表github.com/tc39/proposals按阶段分组更新及时TC39 会议纪要每次会议后都会有详细的 notes 发布在 TC39 的官方仓库里浏览器兼容性查询caniuse.com、kangax.github.io/compat-table/esnext/可以看到各引擎的实现情况。这几个渠道配合起来你就能在几秒钟内判断一个“新特性”到底是“已经能用于生产”还是“社区刚提出来的脑洞”还是“已经凉透了的老黄历”。4. 趋势解读未来两到三年 JavaScript 会往哪里走4.1 并发与调度从“事件循环不够用”到“主动表达优先级”前端应用越来越复杂单线程事件循环逐渐成了性能瓶颈。以前我们用setTimeout拆任务、用requestAnimationFrame做动画、用requestIdleCallback做后台低优先级工作这种“被动调度”不够直观也很难配合现代浏览器的渲染流水线。TC39 显然看到了这个空白。scheduler提案、AsyncContext用于在异步任务之间传递上下文比如 tracing ID都在试着给运行时增加显式表达上下文和优先级的能力。这会带来两个改变一是框架和浏览器之间会有更细粒度的协作React 的并发渲染可能会更深入依赖这类原生能力二是开发者能写出更“可预期”的复杂异步代码而不是把一切交给引擎的任务队列去猜。我个人的看法是这个方向不会在短期内变成人人都会用 API但它会影响“框架设计”和“运行时行为”。真正写业务代码的人可能很少直接调用 Scheduler但框架会用它在合适的时间调度任务最终提升你产品的流畅度。4.2 类型与可读性原生类型注解带来的生态变局TypeScript 今天的地位已经无可替代但它也有自身痛点需要编译步骤、类型系统和运行时逻辑耦合在一起。原生类型注解一旦被正式接受新的项目可能会天然支持“剥掉类型”的运行方式构建链条可以被大幅简化。这带来的替代并不是 TypeScript 的消亡而是 TypeScript 的角色会变得更清晰。检查器、语言服务、编译器依旧存在但它们不再是“必须放在构建流程前面”的不可分割工具。你可以写原生 JS 类型注解也可以继续用完整的 TS 和更丰富的类型构造。两个生态的分界线会逐步清晰一个是“标准 JavaScript 语言”一个是“TypeScript 超集语言”。对前端工程化来说这无异于一次减负。我见过大量团队被构建配置弄得焦头烂额很大一部分都源于“把 TS 编译成 JS”这一步。如果未来浏览器和 Node.js 都能原生跑类型注解语法这一步在很多简单场景下都可以省略。当然“趋势”不等于“马上能落地”。我还是建议保持在当前生产项目继续使用 TypeScript但可以关注--experimental-strip-types之类的实验特性用一些边缘小项目测试不必激进迁移。4.3 正交性与小提案模式回顾近几年进入 Stage 4 的提案你会发现一个规律越来越少出现 ES6 那种“一把梭”的大特征取而代之的是大量正交的、可独立使用的小 API 和语法糖。这种变化对开发者其实更友好因为每个特性的学习成本更低了框架和工具链也可以逐一适配不至于因为一个大版本更新而被迫重构。对新手来说这种趋势也意味着“学习 JavaScript 语法”和“学习 API 标准”这两件事越来越并驾齐驱。过去的 JavaScript 手册可能只有几百页现在官方规范已经厚得像字典。你不需要背下所有 API但应该学会怎么快速查找和判断状态。4.4 前端开发者如何跟上趋势一套低成本学习路径我说一下自己长期坚持的方法每个季度花半天时间扫一遍 TC39 提案列表重点关注所有 Stage 3 的条目挑出和手头项目相关的写一个最小可运行的 Demo然后再去看相关的 polyfill 和原生实现情况。这个方法成本很低但能让你始终处在“提前一两个版本理解语言”的位置。不要试图追每一个提案。对于大多数业务开发者真正值得深入理解的是那些会改变框架写法或生态系统格局的提案比如装饰器、类型注解、Temporal以及那些能减少样板代码的小 API。其余的阶段 0/1 提案看看标题就行深入了解很容易被各种废弃设计消耗时间。5. 常见疑问与避坑记录那些被误解的“未来特性”5.1 Polyfill 和语法转译不是一回事这是我认为最需要被纠正的误区。很多新手看到某个提案说“可以用 polyfill 实现”就在所有代码里直接引用但 polyfill 只能覆盖“API 形态”的特性比如Array.fromAsync、Promise.withResolvers这类运行时方法。对于“语法形态”的特性比如装饰器、管道运算符、可选链当年的?.polyfill 是无能为力的必须借助 Babel、Sweet.js 这类工具在编译阶段转换成目标环境能理解的语法。换句话说判别方法很简单如果提案引入的是一个“函数”或“对象方法”大概率能 polyfill如果提案引入的是一个“新的语法符号”就百分百需要编译工具参与。做反了代码在本地跑得通到生产环境就炸而且报错信息往往很误导人。5.2 怎么判断网上教程说的是不是过时信息JavaScript 相关教程的过时速度可能是所有编程语言里最快的。两年前写“装饰器即将发布”的文章到今天语义已经变了三年前说“Temporal 快定稿了”到今天还在 Stage 3。我踩过最典型的一个坑是照着旧教程配置 Babel 装饰器插件结果version配置名都不存在编译直接崩。判断教程是否过时的最靠谱方式就是去官方提案仓库核对 Stage 状态。如果你看的教程基于 Stage 2 或更早的设计里面一半代码可能都是错的。真正落地到生产环境前以 Stage 3 之后的设计为准。另外可以看一眼文章发布时间和它引用的 proposal 链接如果链接已经 404基本上可以当作历史资料来读。5.3 那些年我们一起追过、最后凉了的提案提案多了自然也有不那么顺利的。最典型的是Object.observe()当年曾被 Chrome 部分实现也出现在很多响应式框架的前瞻文章里后来因为性能和语义问题被正式撤回。另一个例子是“尾调用优化”TCOES6 正式定义了它但各家引擎出于调试困难和栈追踪性能的考虑大多没有完整实现到现在你在实际编码中依然很难依赖它。这提醒我们一个残酷的事实提案不是承诺。TC39 有权利在任何一个阶段撤回或者大幅修改提案。这也是为什么我在很多地方强调“学习和使用要分开”你可以积极了解所有 Stage 3 提案但生产环境里的核心依赖一定要等生态足够成熟否则就会成为那些凉掉的提案的陪葬品。5.4 一些我可以直接复用的避坑清单装饰器版本配置Babel 里必须显式指定version: 2023-11否则默认 legacy 语义会让你写出过时代码Temporal 导入路径不同打包工具和 Node 环境对js-temporal/polyfill的支持有差异注意按项目模块系统选择入口现代浏览器兼容性Stage 4 特性在各浏览器和 Node 版本里的落地时间可以差一年以上用 polyfill 或垫片统一处理更省心不要在业务核心里占坑提案 API 虽然诱人但它的语义存在微小变动可能核心支付、关键数据链路里还是选择稳定库更稳妥。6. 写在最后我的实践体会与后续建议我在不少项目里尝试过各种“未来特性”有成功的也有翻车的但我始终觉得JavaScript 的可预测性恰恰藏在这种提案机制里。它不像很多闭源生态那样突然丢一个不兼容的大版本而是通过公开讨论、分阶段推进、工具链预演让你有充足时间预判变化、准备迁移。我个人目前最关注三件事标准装饰器大规模进入前端框架、Temporal 的正式定稿与原生实现以及类型注解提案能否在 Node.js 的 strip-types 实验中被验证。这三件事无论哪一件正式落地都会显著改变我们常见的编码模式。如果你也想跟进我还真有一个特别推荐的“小技巧”建立一个文件夹专门放各种提案的 minimial reproduction每看到一个有意思的 Stage 3 提案就写一个不超过 20 行的示例代码记录它解决的问题、带来的新写法和可能的坑。几年下来这个文件夹就是你理解 JavaScript 演进的最佳个人资料库。JavaScript 的演进不会停下但好在 TC39 给了我们一个足够清晰的路标。未来的特性会一批批从提案变成现实关键是你要站在正确的时间点去认识它们。
返回列表