开始投入 Harmony
TC39 的 Harmony 项目没有受限于 ES4 开发期间所做的决策,但仍然可以从中参考借鉴。虽然 TC39 仍然会被部分 ES5 项目中的决策所限制,但这项工作现在总体上与 Harmony 的预期方向一致。事实上,在 2008 年下半年和 2009 年的大部分时间里,TC39 在大部分会议时间里关注的都是 ES5。这也为整个委员会提供了一个机会,使他们能以 ES5 规范为起点,熟悉并投入 Harmony 上的工作。
稻草人(Strawman)与目标
在 2008 年 8 月,ECMAScript Wiki 上出现了名为「Harmony 稻草人」的页面,es4-discuss
邮件列表也更名为 es-discuss
g。在 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. 保持语言对业余开发者的愉悦性。
3. 保留语言易于「从小规模开始迭代原型」的性质。
目标
1. 成为如下场景下更好的语言:
一、开发复杂的应用时。
二、开发这些应用所依赖的库(可能包括 DOM)时。
三、开发面向新版的代码生成器时。
2. 切换到可测试的规范,理想情况下这对应于一个主要以 ES5 为宿主的定义解释器。
3. 改善互操作性,尽可能采用事实上的标准。
4. 尽可能保持版本号的简单和线性。
5. 支持在对象层面上可静态验证的安全子集。
手段
1. 尽量减少 ES5 之外所需的额外语义状态。
2. 为以下维度提供语法上的便利:
一、良好的抽象模式。
二、高完整性的模式。
三、经过净化后所定义出的核心语义。
3. 通过可选的版本机制或前置杂注(pragma),去除易混淆或麻烦的结构:
一、考虑使 Harmony 基于 ES5 严格模式。
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. 隔离性,即阻止副作用传播,或仅允许特定引用来传播副作用
* 零授权的制造者式模块(maker-style modules)
* 其他涉及模块的「基础设施 / 上下文 / 内置特性」等的组合
* 浏览器中缺乏隔离:多个互相连接的全局对象
3. 虚拟化,用于分层的客体代码托管,并连接不同的对象系统,特别是模拟宿主对象
* 代理(Proxy)
* 弱引用或 Ephemeron(类似 WeakMap 的数据结构,译者注)
4. 控制副作用,以便于较简单的迭代和状态机代码
* 有限的 continuation 机制
* 生成器与迭代器
5. 为库与工具赋能,这样 TC39 委员会就不会妨碍库的演进
* Object.hashcode
* 某种字节数组
* 值类型(用于十进制小数运算等)
6. 语言改革,需要「更好的胡萝卜」来引导用户远离不好的形式
* 块级作用域中的 let、const 和函数
* 默认参数、剩余参数(rest parameter)和展开运算符(spread operator)
* 解构(destructuring)
7. 版本化,因为新语法是 Harmony 的一部分
* 本主题意在尽量减少选择性使用的版本特性,从而简化迁移,并为未来的下一版做准备
图 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 Aharon | Microsoft | Waldemar Horwat | |
Douglas Crockford | Yahoo! (Phone) | Mark Miller | |
Brendan Eich | Mozilla | John Neumann | Ecma |
Cormac Flanagan | UCSC | Alex Russell | |
David Fugate | Microsoft | Mike Samuel | |
Dave Herman | Mozilla | István Sebestyén | Ecma |
Luke Hoban | Microsoft | Sam Tobin-Hochstadt | Northeastern Univ |
Bill Frants | Periwinkle (guest) | Allen Wirfs-Brock | Mozilla |
图 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。
梦想:绑定与作用域。块级作用域的声明和自由变量引用,属于早期(解析时)错误:
let block_scoped = "yay!"
const REALLY = "srsly"
function later(f, t, type) {
setTimeout(f, t, typo) // EARLY ERROR
}
ES2015 现实:支持块级作用域的 let
和 const
声明,但 1JS 令自由变量引用不属于早期错误。
梦想:函数声明的改进。消除 function
关键字,隐式 return
最后的表达式,即可为不存在自由变量的函数消除冗余闭包:
const #add(a, b) { a + b }
#(x) { x * x }
ES2015 现实:箭头函数取代了 #
符号,仅对带有表达式体的箭头函数采用隐式 return
。对象字面量和类语句体中使用了简洁的方法。至于是否做对上层不可见的闭包优化,则交由实现决定:
const add = (a, b) => a + b // 表达式体隐式返回
x => x * x
x => { console.log(x); return x * x } // 语句体需要显式返回
// 对象字面量与类中的方法定义
class {
add(a, b) { return a + b } // 不支持表达式体
}
梦想:使用词法作用域的 this。在 #
号函数中,this
基于词法作用域绑定:
function writeNodes() {
this.nodes.forEach(#(node) {
this.write(node)
})
}
ES2015 现实:对于 this
和其他函数级作用域的隐式绑定,都会在箭头函数中使用词法绑定:
function writeNodes() {
this.nodes.forEach(node => this.write(node))
}
梦想:记录(record)与元组(tuple)。支持不可变的数据结构,并支持内容层面的等价性:
const point = #{ x: 10, y: 20 }
point === #{ x: 10, y: 20 } // true
ES2015 现实:未支持。这一特性过于接近「可扩展的值类型」的概念,这在 Harmony 中并未获得充分开发。
梦想:剩余参数、展开与解构。支持可变长度参数列表的语法,可将数组展开到参数列表与数组字面量,并从数组和对象中提取组件。
function printf(format, ...args) {
/* 将 args 作为真实数组使用 */
}
function construct(f, a) {
return new f(...a)
}
let [first, second] = sequence
const { name, address, ...misc } = person
ES2015 现实:除了 ES2015 中不支持 ...
运算符的对象解构外,与设想完全相同。对象解构特性在后续版本中已经加入。
梦想:模块。一种简单的模块化设计,支持在浏览器中异步加载。
module M {
module N = "http://N.com/N.js"
export const K = N.K // N.K 的值
exported export #add(x, y) { x + y }
}
ES2015 现实:每个文件一个模块,没有明确的模块定义定界符。支持更多的 import
和 export
形式。基于绑定而非模块间共享的值。
// http://M.com/M.js 的内容
export {K} from "http://N.com/N.js" // N.K 所 export 的绑定
export const add = (x, y) => x + y
梦想:迭代。对无括号的 for-in
语句进行扩展,使其能与「基于 proxy 的标准库」或「用户定义的生成器函数」所提供的迭代器一起工作。
module Iter = {"@std:Iteration"}
import Iter.{keys,values,items,range}
for k in keys(o) { append(o[k]) }
for v in values(o) { append(v) }
for [k,v] in items(o) { append(k, v) }
for x in o { append(x) }
#sqgen(n) { for i in range(n) yield i*i }
return [i * i for i in range(n)] // 数组推导
return (i * i for i in range(n)) // 生成器推导
ES2015 现实:1JS 鼓励使用 for-of
语句,以取代通过依赖模块和 proxy 来重载 for-in
的行为。内置的集合类也定义出了标准的 key / value / entries 协议。出于对未来前景的考量,推导式在 Harmony 开发的后期被放弃了。
for (k of o.keys()) append(o[k])
for (v of o.values()) append(v)
for ([k,v] of o.entries()) append(k, v)
for (x of o) append(x) // o 提供了其默认迭代器
function *sqgen(n) {for (let i of Array(n).keys) yield i*i } // 一个生成器
梦想:无括号的语句。这是更为现代的语法,在复合语句中取消了原先必需的小括号:
if x > y { alert("paren-free") }
if x > z return "brace -free"
if x > y { f() else if x > z { g() }
ES2015 现实:被认为过于激进而被 TC39 拒绝,未纳入规范。1JS 要求继续承认旧的语法形式,新旧形式的混合导致了设计和使用上额外的复杂性。