开始投入 Harmony

TC39 的 Harmony 项目没有受限于 ES4 开发期间所做的决策,但仍然可以从中参考借鉴。虽然 TC39 仍然会被部分 ES5 项目中的决策所限制,但这项工作现在总体上与 Harmony 的预期方向一致。事实上,在 2008 年下半年和 2009 年的大部分时间里,TC39 在大部分会议时间里关注的都是 ES5。这也为整个委员会提供了一个机会,使他们能以 ES5 规范为起点,熟悉并投入 Harmony 上的工作。

稻草人(Strawman)与目标

在 2008 年 8 月,ECMAScript Wiki 上出现了名为「Harmony 稻草人」的页面,es4-discuss 邮件列表也更名为 es-discussg。在 Harmony 项目提出后,es-discuss 上爆发了关于其潜在特性的新讨论。根据当时的工作流程,新的想法会在 es-discuss 或 TC39 会议上提出。如果 TC39 的成员认为某个想法有价值,他们会写出一份初步的设计或特性描述,并将其发布到稻草人 Wiki 页面上。随后这个「稻草人」将在 TC39 会议上进行展示。根据委员会的反应,该想法要么被放弃,要么被反复修改以继续完善。到 2008 年 11 月 21 日,稻草人 Wiki 页面 [TC39 Harmony 2008] 中共列出了以下条目:

  • class
  • const
  • lambda
  • 词法作用域
  • 命名
  • 返回到标签
  • 类型

除了这里的 class 还是个占位符之外,所有条目都指向了一份由 Dave Herman 简要撰写的稻草人提案。

对于 Harmony 中可能的特性,人们进行了广泛的讨论。到 2009 年夏天,委员会决定进一步促进这项工作形成体系。在 2009 年 7 月的会议 [TC39 2009a] 上,TC39 成员决定是时候定义出 Harmony 的目标了。他们认为 ES3.1 的目标 [Crockford 2008a] 在此仍然适用,主要只是在其基础上做一些补充和改进。Brendan Eich [2009a] 发布了这些目标的新版本。其最终产物是如图 38 所示的「Harmony 目标说明」。

  1. 需求
  2. 1. 新特性需要具体的示例。
  3. 2. 保持语言对业余开发者的愉悦性。
  4. 3. 保留语言易于「从小规模开始迭代原型」的性质。
  5. 目标
  6. 1. 成为如下场景下更好的语言:
  7. 一、开发复杂的应用时。
  8. 二、开发这些应用所依赖的库(可能包括 DOM)时。
  9. 三、开发面向新版的代码生成器时。
  10. 2. 切换到可测试的规范,理想情况下这对应于一个主要以 ES5 为宿主的定义解释器。
  11. 3. 改善互操作性,尽可能采用事实上的标准。
  12. 4. 尽可能保持版本号的简单和线性。
  13. 5. 支持在对象层面上可静态验证的安全子集。
  14. 手段
  15. 1. 尽量减少 ES5 之外所需的额外语义状态。
  16. 2. 为以下维度提供语法上的便利:
  17. 一、良好的抽象模式。
  18. 二、高完整性的模式。
  19. 三、经过净化后所定义出的核心语义。
  20. 3. 通过可选的版本机制或前置杂注(pragma),去除易混淆或麻烦的结构:
  21. 一、考虑使 Harmony 基于 ES5 严格模式。
  22. 4. 支持虚拟化,允许对宿主对象的模拟。

图 38. 2009 年 7 月的 Harmony 目标说明 [Eich 2009a]。

倡导者模型

Dave Herman 向委员会建议,认为委员会应该采用一种名为「倡导者模型」的开发方式87。基于这种模型,应由一位或一小组成员共同对一项单独的特性负责。倡导者(champion)需要写出最初的稻草人提案,并持续对其进行改进,直到提案可以被整合到实际规范中为止。从提出最早的稻草人提案起,倡导者还需要随提案发展向整个委员会做报告,并接受来自委员会和其他评审者的反馈。这些反馈意见也由倡导者消化,并据此决定是否对提案进行更新。基于倡导者模型,委员会就应该不会在倡导者报告过程中陷入「委员会设计」的行为了。不过最后仍然需要委员会全体达成一致,以决定将最终提案纳入规范。

委员会接受了 Herman 对倡导者模型的提案,并总体上有效地使用了这一模型。但这种机制也有崩溃的时候。这一时期的核心会员群体相对较小,技术能力也很强。他们有时根本抵挡不住「由委员会做一些设计」的诱惑,有时这其实是在提案上取得进展的最有效方式。有时会出现多位倡导者,他们会对某一特定特性或设计问题提出不同的解法和提案。在这种情况下,如果相互竞争的倡导者们不能就一份共同的提案达成一致,委员会就必须选择一个提案,或在某些情况下拒绝所有相互竞争的提案。

选择特性集

在 2009 年、2010 年和 2011 年上半年的大部分时间里,TC39 的倡导者们都在致力于开发稻草人提案。他们与委员会一起审查这些提案,并试图获得必要的共识,以便将其推进到获得接受的状态。到 2009 年 8 月,稻草人页面 [TC39 Harmony 2009] 上的提案数量已从最初的 7 个发展到了 21 个。到 2010 年初,Harmony 特性集的大致形态开始出现。Brendan Eich [2010a] 将它们组织成了一系列主题(见图 39),并添加到了介绍 Harmony 目标的页面。到 2010 年 12 月,稻草人页面 [TC39 Harmony 2010b] 上的提案已经增加到了 66 个,另有 17 份提案 [TC39 Harmony 2010a] 已确认被推迟或放弃。到 2011 年 5 月初,稻草人页面 [TC39 Harmony 2011c; Appendix N] 有超过 100 个条目,而「已批准提案」的页面 [TC39 Harmony 2011a] 有 17 个条目。

  1. 主题
  2. 1. 模块化,换句话说即如何划分源码单元,以对外部用户隐藏内部细节
  3. 2. 隔离性,即阻止副作用传播,或仅允许特定引用来传播副作用
  4. * 零授权的制造者式模块(maker-style modules
  5. * 其他涉及模块的「基础设施 / 上下文 / 内置特性」等的组合
  6. * 浏览器中缺乏隔离:多个互相连接的全局对象
  7. 3. 虚拟化,用于分层的客体代码托管,并连接不同的对象系统,特别是模拟宿主对象
  8. * 代理(Proxy
  9. * 弱引用或 Ephemeron(类似 WeakMap 的数据结构,译者注)
  10. 4. 控制副作用,以便于较简单的迭代和状态机代码
  11. * 有限的 continuation 机制
  12. * 生成器与迭代器
  13. 5. 为库与工具赋能,这样 TC39 委员会就不会妨碍库的演进
  14. * Object.hashcode
  15. * 某种字节数组
  16. * 值类型(用于十进制小数运算等)
  17. 6. 语言改革,需要「更好的胡萝卜」来引导用户远离不好的形式
  18. * 块级作用域中的 letconst 和函数
  19. * 默认参数、剩余参数(rest parameter)和展开运算符(spread operator
  20. * 解构(destructuring
  21. 7. 版本化,因为新语法是 Harmony 的一部分
  22. * 本主题意在尽量减少选择性使用的版本特性,从而简化迁移,并为未来的下一版做准备

图 39. 2010 年的 Harmony 特性主题 [Eich 2010a]。

2009 年,Brendan Eich [TC39 2009b] 建议 TC39 将 2012 年 6 月作为Ecma GA 通过「ES.next」的目标日期,并将特性冻结的目标日期定为 2011 年 5 月。随着 5 月目标日期的临近,规范明显还无法在 2012 年 6 月完成。但起草一份规范所承诺的特性列表以便专注于其开发,仍然有其意义所在。5 月会议 [TC39 2011b] 的大部分时间用于对稻草人列表进行分类,并就哪些剩余的稻草人提案将推进到「Harmony 提案」状态达成了共识。每份稻草人提案都先经过讨论,然后再去衡量是否有共识来推进它。在经过最低限度的审查后,一些提案获得推进,另一些则被拒绝。对于其他代表重要特性的提案,虽然委员会对当时相应的稻草人不够满意,但它们也得到了推进。这些提案被当作占位符,等待后续开发改进后的提案。如模块和类即均以此方式处理。最终的 Harmony 特性集并未在会议上被严格冻结。随着 ES.next 开发的继续,也有一些提案被加入和放弃。但此次会议所列出的提案清单,已经确立了后来 ES2015 的大致形态。图 40 列出了 5 月会议的参会者,附录 O 则展示了会后的 Harmony 提案页面 [TC39 Harmony 2011b]。

Avner AharonMicrosoftWaldemar HorwatGoogle
Douglas CrockfordYahoo! (Phone)Mark MillerGoogle
Brendan EichMozillaJohn NeumannEcma
Cormac FlanaganUCSCAlex RussellGoogle
David FugateMicrosoftMike SamuelGoogle
Dave HermanMozillaIstván SebestyénEcma
Luke HobanMicrosoftSam Tobin-HochstadtNortheastern Univ
Bill FrantsPeriwinkle (guest)Allen Wirfs-BrockMozilla

图 40. 2011 年 5 月 TC39 特性筛选会的参会者 [TC39 2011b]。

开始编写规范

作为项目编辑,Allen Wirfs-Brock 全权负责根据 TC39 倡导者开发的 Harmony 提案,来创建 ES.next 规范文档。在微软,他的职责被分散在 TC39 相关工作与其他项目之间。2010 年 12 月,他离开微软加入 Mozilla,专注于 ES Harmony。

ES4 和 ES5 的经验使 Wirfs-Brock 明白,持续不断地对具体规范文档的开发,是完成新版标准的关键。2011 年 6 月 22 日,他怀着坚定的决心打开了最近完成的 ES5.1 规范的源文件,将封面页改为「第 6 版草案」,并将其保存为基准 ES6 规范草案。然后,他立即开始根据 5 月份的特性分类与委员会两年来做出的其他决定,在草案中编辑新材料。7 月 12 日,他发布了「ES.next 规范的第一份工作草案」[Wirfs-Brock et al. 2011a, b]。图 41 是该草案的变更摘要。这是委员会发布的 38 份草案中的第一份,最后一份草案则于 2015 年 4 月 14 日发布到了 Wiki 上 [Wirfs-Brock et al. 2015a, c]。

  • 5.1.4 引入补充语法的概念。
  • 5.3 引入静态语义规则的概念。
  • 8.6.2 等处取消了 [[Class]] 内部属性,增加了各种内部特征属性作为替代。
  • 10.1.2 定义了「扩展代码」的概念,即指可能使用新版 ES.next 语法的代码。一并重新定义的还有「严格代码」,即 ES5 严格模式代码或扩展代码。
  • 11.1.4 增加了在数组字面量中使用展开运算符的语法和语义。
  • 11.1.5 增加了属性值简写的语法和语义,以及多种辅助抽象操作的语义。
  • 11.2, 11.2.4 增加了参数列表中展开运算符的语法和语义。
  • 11.13 增加了解构赋值运算符的语法和语义。
  • 12.2 增加了 BindingPattern 语法和部分语义,以支持在声明与形参列表中的解构。
  • 13 增加了对形参列表中剩余参数、参数默认值和模式解构的语法支持,并为它们提供了静态语义。但这种参数的实例化还未完成。对这类增强后的形参列表,也定义了实参列表的「长度」。
  • 15 说明了此条目的函数规范,实际上是 [[Call]] 内部方法的定义。
  • 15.2.4.2 重新规定 toString 不使用 [[Class]]。注意未来仍然需要增加一种明确的扩展机制。
  • Annex B 改名为面向 Web 浏览器 ES 实现的规范化可选特性。

图 41. 首份 ES6 草案的变更日志 [Wirfs-Brock et al. 2011a, reformatted]。

One JavaScript

从 Harmony 项目启动起,TC39 就假定需要某种显式的「选择性使用」(opt-in)机制,来使用很多(甚至可能是所有)的新 Harmony 特性。这是从 ES4 时代延续下来的。在 ES4 时代,很多提案都包含了会使一些现有 JavaScript 程序失效的破坏性变更。Harmony 的进程对于纳入破坏性变更而言比较保守,但还是有所考虑的。在 Harmony 开发的前三年,具体的选择机制还没有确定,但也经常受到讨论。第一份 ES6 草案引入了「扩展代码」的概念,它是 ES5 严格代码的超集,但还没有包含对具体选择机制的描述。一些可供考虑的替代方案包括:使用 HTML <script> 元素属性从外部进行选择;使用新的 use mode 杂注语句;使用某种分隔的语法形式;添加一种类似于 "use strict" 的新指令等。有人担心这样下去将来会有多少种模式,难道标准的每个大版本里都需要选择性地使用一种新模式吗?这似乎对语言用户和实现者而言都是个重大的复杂性负担。

Dave Herman [2011b] 在题为「ES6 不需要 opt-in」的 es-discuss 消息中认为,破坏性变更应该非常有限,并且仅限于在封装为 ES6 模块的代码内。绝大多数特性应该是非破坏性的,这样无论它们是否出现在模块中,都应该表现得完全一致。在某些情况下,这可能需要重新设计一些特性。在少数情况下,设想中的特性可能不得不为此而放弃。在对这条 es-discuss 消息的 150 多条回复中,这些想法逐渐得到了完善。在接下来的 TC39 会议上,Herman [2012] 做了一次名为「One JavaScript」的演讲,其中介绍了对这些想法的提炼。这里的关键在于,未来的程序员与 ECMAScript Harmony 的实现者们,应该能够用一种统一的 JavaScript 语言来思考,而不用考虑模式、版本或方言。TC39 有责任使 ES.next 的设计与此观点保持一致。会议的大部分时间都在讨论这条命题,以及它对各种 Harmony 特性的影响。大家的共识是尽量让「1JS」适用于 Harmony。在下一份规范草案 [Wirfs-Brock et al. 2012a] 中,扩展代码的概念被删除了。同时人们也做了各种其他的修改,以消除潜在的破坏性变更。

Brendan 的梦想

2011 年 1 月,在 Harmony 上投入了两年多的工作后,Brendan Eich [2011b] 发表了一篇名为《我的 Harmony 梦想》的博客文章,其中提出了一些关于语言进化和标准委员会的观点。文中核心则给出了他希望中「Harmony JavaScript 应该是什么样子」的示例。

……我想提出一个全新的 JavaScript Harmony 愿景。当然,这里的概念性尝试还(暂时)不够标准,但也不是一些随意而糟糕的衍生品。这些东西确实可能成为现实。如果有你们的帮助,它们会更有可能实现,并且能实现得更好(关于如何参与,可参见本文末尾)。

我正在模糊 Ecma TC39 目前的共识与我的想法之间的界限。这里的共识包括 Harmony 项目,以及 TC39 上一些人赞成的 Harmony 稻草人提案。我这么做是故意的,因为我认为 JS 需要一些新的概念上的完整性。它不需要安全的委员会设计,不管是那种「让我们把所有提案联合起来」的方法(这在 TC39 上是行不通的),还是盲目地「让我们求出提案间的交集,如果结果还是空集,那就这样算了吧」的方法(这也是行不通的,但这是更可能的坏结果),都是不可行的。

他介绍了各种场景下如何使用 ES5 特性进行编码的示例,以及如何在他梦想的 Harmony 中表达同等内容的替代性示例。这些设想中的例子,展示了 Harmony 提案的中间阶段,以及它们是如何演变成实际 ES2015 特性的。他提出的一些内容并未纳入 ES2015 中,大多数特性最后在某些方面发生了变化。另外也有必要做出其他的改动,因为 1JS 理念消除了对现有特性的语法和语义进行选择性修改的可能性。

为了解这些特性的演化,这里将比较 Brendan Eich 在 2011 年的「梦想」88和最终成为现实的 ES2015。

梦想:绑定与作用域。块级作用域的声明和自由变量引用,属于早期(解析时)错误:

  1. let block_scoped = "yay!"
  2. const REALLY = "srsly"
  3. function later(f, t, type) {
  4. setTimeout(f, t, typo) // EARLY ERROR
  5. }

ES2015 现实:支持块级作用域的 letconst 声明,但 1JS 令自由变量引用不属于早期错误。

梦想:函数声明的改进。消除 function 关键字,隐式 return 最后的表达式,即可为不存在自由变量的函数消除冗余闭包:

  1. const #add(a, b) { a + b }
  2. #(x) { x * x }

ES2015 现实:箭头函数取代了 # 符号,仅对带有表达式体的箭头函数采用隐式 return。对象字面量和类语句体中使用了简洁的方法。至于是否做对上层不可见的闭包优化,则交由实现决定:

  1. const add = (a, b) => a + b // 表达式体隐式返回
  2. x => x * x
  3. x => { console.log(x); return x * x } // 语句体需要显式返回
  4. // 对象字面量与类中的方法定义
  5. class {
  6. add(a, b) { return a + b } // 不支持表达式体
  7. }

梦想:使用词法作用域的 this。在 # 号函数中,this 基于词法作用域绑定:

  1. function writeNodes() {
  2. this.nodes.forEach(#(node) {
  3. this.write(node)
  4. })
  5. }

ES2015 现实:对于 this 和其他函数级作用域的隐式绑定,都会在箭头函数中使用词法绑定:

  1. function writeNodes() {
  2. this.nodes.forEach(node => this.write(node))
  3. }

梦想:记录(record)与元组(tuple)。支持不可变的数据结构,并支持内容层面的等价性:

  1. const point = #{ x: 10, y: 20 }
  2. point === #{ x: 10, y: 20 } // true

ES2015 现实:未支持。这一特性过于接近「可扩展的值类型」的概念,这在 Harmony 中并未获得充分开发。

梦想:剩余参数、展开与解构。支持可变长度参数列表的语法,可将数组展开到参数列表与数组字面量,并从数组和对象中提取组件。

  1. function printf(format, ...args) {
  2. /* 将 args 作为真实数组使用 */
  3. }
  4. function construct(f, a) {
  5. return new f(...a)
  6. }
  7. let [first, second] = sequence
  8. const { name, address, ...misc } = person

ES2015 现实:除了 ES2015 中不支持 ... 运算符的对象解构外,与设想完全相同。对象解构特性在后续版本中已经加入。

梦想:模块。一种简单的模块化设计,支持在浏览器中异步加载。

  1. module M {
  2. module N = "http://N.com/N.js"
  3. export const K = N.K // N.K 的值
  4. exported export #add(x, y) { x + y }
  5. }

ES2015 现实:每个文件一个模块,没有明确的模块定义定界符。支持更多的 importexport 形式。基于绑定而非模块间共享的值。

  1. // http://M.com/M.js 的内容
  2. export {K} from "http://N.com/N.js" // N.K 所 export 的绑定
  3. export const add = (x, y) => x + y

梦想:迭代。对无括号的 for-in 语句进行扩展,使其能与「基于 proxy 的标准库」或「用户定义的生成器函数」所提供的迭代器一起工作。

  1. module Iter = {"@std:Iteration"}
  2. import Iter.{keys,values,items,range}
  3. for k in keys(o) { append(o[k]) }
  4. for v in values(o) { append(v) }
  5. for [k,v] in items(o) { append(k, v) }
  6. for x in o { append(x) }
  7. #sqgen(n) { for i in range(n) yield i*i }
  8. return [i * i for i in range(n)] // 数组推导
  9. return (i * i for i in range(n)) // 生成器推导

ES2015 现实:1JS 鼓励使用 for-of 语句,以取代通过依赖模块和 proxy 来重载 for-in 的行为。内置的集合类也定义出了标准的 key / value / entries 协议。出于对未来前景的考量,推导式在 Harmony 开发的后期被放弃了。

  1. for (k of o.keys()) append(o[k])
  2. for (v of o.values()) append(v)
  3. for ([k,v] of o.entries()) append(k, v)
  4. for (x of o) append(x) // o 提供了其默认迭代器
  5. function *sqgen(n) {for (let i of Array(n).keys) yield i*i } // 一个生成器

梦想:无括号的语句。这是更为现代的语法,在复合语句中取消了原先必需的小括号:

  1. if x > y { alert("paren-free") }
  2. if x > z return "brace -free"
  3. if x > y { f() else if x > z { g() }

ES2015 现实:被认为过于激进而被 TC39 拒绝,未纳入规范。1JS 要求继续承认旧的语法形式,新旧形式的混合导致了设计和使用上额外的复杂性。