ES2015 语言特性

Harmony 提案 Wiki 页面 [TC39 Harmony 2014] 最终版本中所列出的提案,被开发成了几十种语言及其标准库的新特性与扩展特性。典型提案在被纳入规范草案之前,要经过多次反复的迭代。有些提案在纳入规范草案后,还会继续进行改进。一些提案最终被放弃审议,或推迟到未来的版本中。

以下各节将深入探讨几项重要提案的发展历史,并总结其他重要特性的细节。

Realms、Jobs、Proxies 和元对象编程(MOP)

Harmony 的目标之一,在于使异质对象不分其为内置还是由宿主定义,均能实现自托管,并完全确定其由 Web 浏览器所实现的语义扩展机制。为支持这一目标,需要完善某些 ECMAScript「虚拟机」中现有的抽象,并进一步增加新的抽象,以确定新的(或不够明确的)语言特性。

「Realm」[Wirfs-Brock 2015a, pg. 72] 是一种新的规范抽象。引入它的目的,是为了支持在单个 ECMAScript 执行环境中描述多个全局命名空间的语义。Realm 能支持 HTML 页框的语义,这是 ECMAScript 自 ES1 以来一直忽略的浏览器特性。而「Job」[Wirfs-Brock 2015a, pg. 76] 这种规范抽象的加入,是为了确定性地定义 ECMAScript 执行环境该如何将多个脚本依次执行到完成(run-to-completion)。基于 Job 所提供的方法,能解释由浏览器和其他 JavaScript 宿主所提供的「事件派发」和「延迟回调」的语义。它们还为定义 ES2015 中 Promise 的语义建立了基础。

ES1 所提供的内部方法,基本上是个残缺的元对象协议。在对各种内置对象和宿主提供的对象做属性访问时,会有各类可见的语义区别。基于内部方法,可以将这些区别解释为它们在内部方法规范上的差异。但在 ES2015 之前,内部方法的语义还不够完整和规范,其使用也不够一致。为了「驯服」宿主对象,实现异质对象的自托管,并支持对象能力的隔离层g [Van Cutsem and Miller 2013]。ES1 到 ES5 中所设计的内部方法,被转换成了一种明确的元对象编程(MOP)。

JavaScript 代码要想定义异质对象,就必须能为这些对象所用的内部方法提供相应的实现。这个特性是由 ES2015 中的 Proxy 对象 [Wirfs-Brock 2015a, pg. 495] 提供的。新版 ES4 提出了一种名为「catchalls」[TC39 ES4 2006a] 的机制,从而让 JavaScript 代码能逐对象地覆盖当「试图访问某个属性,或调用某个不存在的方法」时发生的默认动作。这个「catchalls」机制的目的,是改进 JavaScript 1.5 的非标准 __noSuchMethod__ 机制 [Mozilla 2008a]。在 Harmony 中,Brendan Eich [2009b; 2009d] 引入了所谓的「动作方法」(action method)概念,使其能动态附加到对象上,从而令新版 ES4 的 catchalls 更进一步通用化。在对某个对象执行某些语言操作时,如果该对象上已定义了相应的动作方法,则会调用该方法。可用的动作集与 ES5 的内部方法集类似,但不是它们的直接映射。这里有个悬而未决的问题,即这些动作是在执行所有属性访问时触发,还是仅当访问不存在的属性时触发。Eich 所设计的用于将动作附加到对象上的 API,是以 ES5 对象反射函数为基础的:

  1. // Harmony Catchall 提案
  2. var peer = new Object;
  3. Object.defineCatchAll(obj, {
  4. // 加入支持数组式行为的动作
  5. has: function (id) { return peer.hasOwnProperty(id); },
  6. get: function (id) { return peer[id]; },
  7. set: function (id, value) {
  8. if ((id >>> 0) === id && id >= peer.length) peer.length = 1 + id;
  9. peer[id] = value
  10. },
  11. add: function (id) {
  12. Object.defineProperty(obj, id, {
  13. get: function () { return peer[id]; },
  14. set: function (value) { peer[id] = value; }
  15. })
  16. },
  17. // 其他动作的定义...
  18. });

在这个例子中,属性 hasgetsetadd 提供了动态附加到 obj 对象上的所有动作。各动作函数可在词法上共享对 peer 对象的访问,这就在 objpeer 之间建立了一对一的关联。这些处理函数共同使用 peer 来支持对 obj 自有属性的存储。它们还会动态更新 peer 对象的 length 属性值,因此该值总比用作属性名的最大整数大 1。

在 Brendan Eich 的 catchall 提案之后不久,Tom Van Cutsem 和 Mark Miller [2010a; 2010b] 又提出了另一种设计。这就是「基于代理的 catchall 提案」[Van Cutsem 2009],它定义了一套分层的对象交互 API。Proxy 提案的目的是支持对虚拟对象的定义,例如在安全的「基于对象能力」式系统中,定义出用于实现隔离的隔离层对象。TC39 基本认可了 Proxy 稻草人,并很快将其作为 Harmony 提案接受。

这份提案引入了 Proxy 对象的概念。提案没有扩展出具侵入性动作方法的基础对象,而是选择创建一个与处理器对象(handler object)相关联的 Proxy 对象,其中的方法称之为「trap」。Trap 会由语言操作而触发。通过处理器函数,可以完全定义出语言操作所用的对象行为。Trap 既可能是自包含的,也可能通过词法捕获的形式,与「对处理器函数可见的已有对象」一起使用。如下所示 [Van Cutsem and Miller 2010c]:

  1. // 最早的 Harmony Proxy 提案
  2. // 一个进行简单转发的代理
  3. function makeHandler(obj) {
  4. return {
  5. has: function (name) { return name in obj; },
  6. get: function (rcvr, name) { return obj[name]; },
  7. set: function (rcvr, name, val) { obj[name] = val; return true; },
  8. enumerate: function () {
  9. var res = []; for (name in obj) { res.push(name); }; return res;
  10. },
  11. delete: function (name) { return delete obj[name]; }
  12. };
  13. }
  14. var proxy = Proxy.create(makeHandler(o), Object.getPrototypeOf(o));

在这个例子中,makeHandler 是用于创建处理器对象的辅助函数,其 trap 在词法上共享对「作为参数传递给 makeHandler 的对象」的访问。传递给 makeHandler 的对象可能是一个新创建的对象,这时它的作用类似于 catch-all 例子中的 peer 对象。另外,被传递的对象也可以是一个已有的对象。这时,trap 可以将部分或全部被截获的操作转发给该对象。在这种情况下,obj 对象的角色就相当于「被转发代理」的目标。

通过将 trap 方法放在处理器对象中的方式,可以避免它与基础对象属性的名称相冲突。提案中定义了 7 种基本 trap、6 种派生 trap89,以及 2 种针对函数对象的 trap。和 catchall 提案类似地,trap 和 ES5 的内部方法相接近,但也不是 ES5 内部方法的直接映射。ES5 中为 [[GetOwnProperty]][[DefineOwnProperty]] 内部方法建立了某些不可违背的一致性规则 [Wirfs-Brock 2011b, page 33]。而对 ES2015 来说,有个棘手的问题就是如何在实行90这些规则的同时,对「被冻结或密封的对象」与「不可配置的属性」进行虚拟化。

在对原始 Proxy 提案做原型建设后,Van Cutsem [2011] 宣布了重大修订:

几周前,Mark 和我坐在一起研究了 proxy 的一些现存问题,特别是如何让 proxy 更好地处理不可配置的属性和不可扩展的对象。其结果就是我们所说的「直接代理」:在我们的新提案中,proxy 总是另一个「目标」对象的包装器。只要以这种方式稍微转变我们对 proxy 的看法,很多早先开放的问题就不复存在了。并且这样一来,proxy 的开销在某些情况下可能会大大减少。

在「直接代理」的提案 [Van Cutsem and Miller 2011a, b, 2012] 中,目标对象(以下例子中的 o)类似于转发代理例子中传递给 makeHandler 的对象。它作为 Proxy 对象的内部状态而保存,并在调用 trap 时作为一个显式参数来传递。因为 Proxy 了解目标对象的信息,所以它可以在使用目标对象时,确保其符合必要的一致性规则。以下是直接代理版本的 Proxy 转发示例:

  1. // Harmony 直接代理提案
  2. // 一个进行简单直接转发的代理
  3. var Proxy(o, {
  4. // 处理器对象
  5. has: function (target, name) {
  6. return Reflect.has(target, name)
  7. },
  8. get: function (target, name, rcvr) {
  9. return Reflect.get(target, name, rcvr)
  10. },
  11. set: function (target, name, val, rcvr) {
  12. return Reflect.set(target, name, val, rcvr)
  13. },
  14. enumerate: function (target) {
  15. return Reflect.enumerate(target)
  16. },
  17. // ...
  18. });

这里 Reflect 对象的方法对应于标准的内部方法。它们使处理器函数能直接调用对象的内部方法,而非使用隐式调用它们的 JavaScript 代码序列。在直接代理的设计中,最初主要根据 ES5 的内部方法,定义出了 16 种不同的 trap。设计中还发现对于某些对象的内部操作,由于其没有用内部方法来定义,所以无法被 Proxy 拦截。Tom Van Cutsem、Mark Miller 和 Allen Wirfs-Brook 共同开发了 Harmony 内部方法和 Proxy 的 trap,使它们保持一致,并足以表达 ECMAScript 规范和宿主对象中所定义的所有对象行为。其具体的实现手段是增加新的内部方法,以及将一些不可截取的操作,重新定义为基础级、可捕获的常规方法调用。此外提案还定义了每个内部方法的关键一致性规则。ECMAScript 的实现和宿主都必须确保符合这些一致性规则,而 Proxy 可以对自托管的异质对象实行91这些规则。图 44 是对 ES2015 中元对象编程的概述:

ES5 内部方法ES6 内部方法ES6 Proxy Traps 与反射方法
[[Canput]]
[[DefaultValue]]
[[GetProperty]]
[[HasProperty]][[HasProperty]]has
[[Get]][[Get]]get
[[GetOwnProperty]][[GetOwnProperty]]getOwnPropertyDescriptor
[[Put]][[Set]]set
[[Delete]][[Delete]]deleteProperty
[[DefineOwnProperty]][[DefineOwnProperty]]defineProperty
[[Call]][[Call]]apply
[[Construct]][[Construct]]construct
[[Enumerate]]enumerate
[[OwnPropertyKeys]]ownKeys
[[GetPrototypeOf]]getPrototypeOf
[[SetPrototypeOf]]setPrototypeOf
[[IsExtensible]]isExtensible
[[PreventExtensions]]preventExtensions

图 44. ES6/ES2015 的元对象协议由规范级内部方法定义,并通过 Proxy 的 trap 和 Reflect 方法进行验证。

在直接代理的设计中,使用了一个封装过的目标对象。但它的设计目的并非提供目标对象的简易透明封装。与其表象相反,代理并不是一种用来记录属性访问或处理「方法未找到」问题的简单方式。为了支持这些用例而朴素实现的 Proxy 对象,通常是不可靠或有错误的。直接代理的核心使用场景,是对象的虚拟化和安全隔离层的创建。正如 Mark Miller [2018] 所解释的那样:

Proxy 和 WeakMap 的最初设计动机,是支持隔离层的创建。单独使用的 proxy 不可能是透明的,也不能合理地达到接近透明的程度。隔离层能合理且几乎透明地模拟 realm 的边界。对于具备私有成员的类而言,这种模拟基本上是完美的。

块级声明作用域

从初版 ES4 起,就有对加入块级声明作用域的诉求。具有类 C 式语言语法经验的程序员,会希望位于 {} 块中的声明属于该块中的局部变量。最早 JavaScript 1.0 中的 var 作用域规则令人惊讶,有时会掩盖严重的错误。其中的一个常见 bug 就是循环中闭包的问题:

  1. // ES3
  2. function f(x) { // 此函数有循环中闭包的 bug
  3. for (var p in x) {
  4. var v = doSomething(x, p);
  5. obj.setCallback(function (arg) { handle(v, p, arg) });
  6. // 全部在循环中创建的闭包都共享 v 和 p 的绑定
  7. // 而不是在每次迭代中使用不同的绑定
  8. }
  9. }

这种手法在操作浏览器 DOM 的代码中很常见——即便是有经验的 JavaScript 程序员,有时也会忘记 var 声明不是块级作用域的。

除非破坏已有代码,否则现有的 var 声明是无法改变为块级作用域的。在新版 ES4 尝试中,已经确定使用关键字 letconst 作为声明,以满足对块级作用域的需求。关键字 let 用于定义可变的变量绑定,而 const 则用于定义不可变的常量绑定。它们的使用并不限于块,而是可以出现在任何能出现 var 声明的地方。新版 ES4 设计团队甚至还制作了写有标语「let 是新的 var」的T恤。Harmony 继承了 letconst 声明,但新版 ES4 工作中仍有许多相关的语义问题尚未得到解答。

ES5 曾考虑增加 const 声明。ES5 规范中包含了可用于确定块级声明绑定语义的抽象。但至于这些语义究竟该如何确定,则并不明显。下面的代码片段说明了一些问题。

  1. // ES2015
  2. { // 外层块
  3. let x = "outer";
  4. { // 内层块
  5. console.log(x);
  6. var refX1 = function () { return x };
  7. console.log(refX1());
  8. const x = "inner";
  9. console.log(x);
  10. var refX2 = function () { return x };
  11. console.log(refX2());
  12. }
  13. }

const 声明之前的内层块中,出现的对 x 的某些引用或所有引用,是否应该是编译时错误呢?还是说它们应该是运行时错误呢?如果它们不是错误,那么是否应该将其解析到 x 的外部绑定呢?或者说内层的 x 在初始化之前,是否应该以 undefined 为默认值?如果在 const 声明之前调用函数 refX1,是否应该和在声明之后调用函数一样,解析到同样的 x 绑定和相同的值呢?如果 x 的内层声明是一个 let 声明,上述所有问题仍然适用。针对这些情况下的引用,Waldemar Horwat [2008a] 描述了四种可能的语义:

  • A1. 词法死区。在同一块中「文本上前于」(textually prior)变量定义而出现的引用,属于错误。
  • A2. 词法窗口。在同一块中「文本上前于」变量定义而出现的引用,进入外部作用域。
  • B1. 临时性死区。在同一块中「临时性前于」(temporally prior)变量定义而出现的引用,属于错误。
  • B2. 临时性窗口。在同一块中「临时性前于」变量定义而出现的引用,进入外部作用域。

Horwat 感谢 Lars Hansen 将「死区」的概念引入讨论。术语「临时性前于」指的是运行时求值顺序。A2 和 B2 是不可取的,因为这使得块中同一名称在不同的位置,可以有不同的绑定。并且在 B2 的情况下,块中某处的名称甚至在不同的时刻,都可以有不同的绑定。A1 是不可取的,因为它妨碍了以这些声明形式来定义相互递归的函数。A2 的缺点在于,它需要对所有引用进行运行时初始化检查,不过这其中有许多可以被编译器基于相当简单的分析来安全地消除。但在花了近两年时间后 TC39 最终达成的共识,是认为新的词法声明形式应具有 B1 的临时性死区(TDZ)语义。这些语义可由下面这些规则来概括:

  • 在一个作用域内,任何名称都只有唯一的一个绑定。
  • letconstclassimport、块级函数声明和形参绑定在运行时是死的,直到初始化为止。
  • 访问或赋值给一个未初始化的绑定,属于运行时错误。

在规范中,上述第一条规则表示为早期错误规则,另外两条则表示为运行时语义算法。

当 Allen Wirfs-Brock 开始将 letconst 集成到规范中时,他发现二者与传统的 varfunction 声明之间,还存在着许多潜在的交互。这导致 TC39 又进行了一轮讨论,就下列补充规则达成了一致意见:

  • 一个名称的多个 var 声明可以存在于任何层级的块嵌套中。它们都指向同一个绑定,其定义会被提升到最接近的外层函数或顶层全局作用域中(ES1 遗留语义)。
  • 允许为同一名称进行多次 var 声明和函数 / 顶层全局作用域内的 function 声明,每个名称对应一个绑定(ES3 遗留语义)。
  • 所有其他在同个作用域中的多重声明,都属于早期错误,包括 var/letlet/letlet/constlet/functionclass/functionconst/class 等。
  • 如果一个块级的 var 声明名称,被提升到了任何同名的外层 letconstclassimport 或块级 function 声明之上,这也属于一个早期错误。
  • 当创建绑定时,var 声明会被自动初始化为 undefined,因此对它们的访问没有 TDZ 限制。

另一组问题则涉及对全局声明的处理。在 ES2015 之前,所有的全局声明都会在宿主环境提供的全局对象上创建属性。但是对象属性并没有像实现临时性死区所需的那样,规定将一个属性标记为未初始化。有一份提案要求把全局层级上新 constletclass 声明的出现,当作是 var 声明。这方面存在先例,因为一些 ES2015 之前的 JavaScript 引擎,已经以这种方式实现了 const 声明。然而这将导致在全局层级上使用新的声明时,会和其他位置上的使用不一致。相比之下 TC39 的共识,则是词法声明规则应尽可能一致地适用于所有类型的作用域。对于全局作用域,varfunction 声明保留了创建全局对象属性的遗留行为,但所有其他声明形式,都会创建不影响全局对象属性的词法绑定。

新的规则不允许应用存在矛盾的 var/let 多重绑定,对类似的冲突而言也是这样的。但例外是那些不使用 varfunction 声明创建的全局对象属性,它们不会导致多次声明之间的冲突。在这些情况下,一个全局的 let/const/class 声明会遮盖名称相同的全局对象属性。这暗含了一条规则,即使用新声明定义的全局变量,不能在单独的脚本中多次定义。

仅仅增加块级作用域的 letconst 声明,还不足以完全消除循环中闭包的隐患。这里还有一个由 for 语句引入的变量作用域问题,即 for (var p in x)。ES2015 解决这个问题的方式,是允许在 for 语句的头部使用 letconst 来代替 var。以这种方式使用的 letconst 会在作用域轮廓中创建一个绑定,这个绑定会在循环体的每次迭代中重新创建。循环 for (const p in x) {body}去糖化g之后,大致如下所示:

  1. // ES2015
  2. // for (const p in x) {body} 的去糖后近似表示
  3. { let $next;
  4. for ($next in x) {
  5. const p = $next;
  6. {body}
  7. }
  8. }

为处理 C 风格的三表达式 for 语句而引入的词法绑定比较复杂,争议也较大。JavaScript 1.0 已经包含了使用 var 声明作为此类语句第一个表达式的能力,所以 letconst 声明在那里应该也可以使用。但是,这种声明所产生的约束力有多大呢?是应该有一个单独且生命周期为整个 for 语句的绑定,还是应该像 for-in 语句那样,为循环的每一次迭代建立一个单独的绑定呢?答案并不明确,因为常见的编码模式是利用第二、三个表达式或循环体中的代码,来更新所声明的循环变量的值,以便在循环的下一次迭代中使用。如果每次迭代都得到一个新的循环变量绑定,就需要自动使用上一次迭代的循环变量最终值,来初始化下一次迭代中的循环变量绑定。大多数类似 C 的语言,都采用了每条 for 语句对应一个单独绑定的方式,而非每次迭代对应一个绑定的方式,这也是 ES6 规范草案最初的做法。但是,这种方式仍然存在循环中闭包的问题。为此,对于使用 let 声明的三表达式语句,规范最终改为每次迭代使用一个绑定,并在迭代之间传递值。事实证明,对于第一个表达式中的 const 声明来说,使用每个循环语句唯一的绑定就足够了,因为此类变量的值不能被 for 头部或循环体中的其他表达式修改。

另一个重要的问题,是在语句块中声明函数时的语义。ES3 有意排除了(第 12 节)对块内函数声明的任何语法或语义规范。但各实现均忽略了这一指导,允许这样的声明——不幸的是,每个主流浏览器实现都为其赋予了不同的语义。不过在某些使用场景 [Terlson 2012] 下,这些语义之间所存在的重叠,是足够进行这样的函数声明,并在所有主流浏览器中都兼容地使用的。根据 ES2015 的词法声明规则,其中一些使用场景将被认为属于非法,或需要改变其含义。若在这些场景下实现新的规则,将会「破坏 Web」。这对严格模式来说不是问题,因为 ES5 已经禁止语言实现在严格模式代码中提供块级函数声明。对于非严格模式的代码,一种方法是效仿 ES3,不指定任何关于块级函数的内容——让每个实现来决定「是否以及如何」将块级函数声明与新的词法声明形式相整合。但这不利于互操作性,也与 1JS 的目标相悖 [TC39 2013b]。与其相反地,TC39 [2013a] 确定了少数几个用例,其中现有的块级函数具备互操作性且有实际用处,但根据新规则却会出现错误。例如:

  1. // 兼容但非标准的 ES3 扩展
  2. function f(bool) {
  3. if (bool == true) {
  4. function g() { /*do something*/ }
  5. }
  6. if (bool == true) g(); // 这在所有主流浏览器中均可用
  7. }

对此的修复方法,是定义一些额外的非严格模式代码规则 [Wirfs-Brock 2015a, Annex B.3.3]。这些规则可以静态地检测那些特定的可互操作用例,并使其合法地与遗留网页相兼容。对于上面的例子,规则会把其代码当作这样:

  1. // ES2015 附录 B 中的去糖化
  2. function f(bool) {
  3. var g; // 如果顶层存在由 let 声明的 g,则属于早期错误
  4. function $setg(v) { g = v }
  5. if (bool == true) {
  6. function g() { /*do something*/ }
  7. $setg(g); // 将本地 g 设为顶层 g 的值
  8. }
  9. if (bool == true) g(); // 引用顶层 g
  10. }

在 2008 年 7 月发起 Harmony 工作的 TC39 会议上,相当多时间都用来讨论「是否应该以及如何」纳入类。在 ES4 的前后两次尝试中,为了开发复杂的类定义语法和语义,人们都付出了巨大的努力。而且这两次尝试中的设计,都需要新的运行时机制来支持。这些设计可以宽泛地描述为「受 Java 启发的类」。

Mark Miller [2008d] 认为,对于类抽象所需的大部分运行时机制,在 ES3 中已经基于 lambda 函数和词法捕获技术实现了。词法捕获技术类似于 Scheme [Dickey 1992; Sussman and Steele Jr 1975],且由 Douglas Crockford [2008b, pages 52-55] 为适应 JavaScript 而进行了修改。这种「lambda 去糖化」的类定义风格,与模块模式实质上是一致的。它表明类只是一个小而轻的模块,其目的就是用来被多次实例化。Miller 称这种方法为「糖式类」(classes as sugar)。

Cormac Flanagan [2008] 将最初对类的讨论总结如下:

EcmaScript(原文如此)需要提供对「具有数据抽象和隐藏的高完整性对象92」更好的支持,也需要更好地支持私有字段和方法……

……我们最初专注于一个简单的、极简的设计,它不支持继承或类型注解,并使用在实例中私有的数据。类名没有单独的命名空间,类对象是一种新的(一等公民)值。

Flanagan 提出的稻草人提案,使用了简单的类定义语法。如下所示:

  1. // Flanagan 的 Harmony Class 稻草人
  2. class Point (initialX , initialY) {
  3. private x = initialX;
  4. private y = initialY;
  5. public getX() { return x };
  6. public getY() { return y };
  7. }

Cormac Flanagan 的提案内容并未完整地「去糖化」,并且包含的语义细节也很少。Mark Miller [2008c; 2009; 2010a] 用类似的表层语法设计对其进行了反驳。Miller 的提案进行了完整的去糖化,不需为类实例提供一种新的运行时对象。在 Miller 的设计中没有继承,所有的方法和实例变量都默认为私有访问。所有的方法和实例变量都被表示为逐实例的词法捕获声明,这些声明只能从类定义的代码体中直接访问。通过类实例对象的属性,提案支持从外部访问公有方法,并为公有实例变量提供了 get 访问器。从外部直接对实例变量赋值是不允许的,并且提案也不使用this 关键字。

Mark Miller 提出的「糖式类」提案所经常受到的一种批评,是认为它创造了太多的对象。具有 n 个方法的类在每次对象实例化时,除了实际的实例对象外,还会隐式创建 n 个特定于实例的闭包对象。对此 Miller 的立场是,去糖化只定义了可见的语义,而实现者可以自由开发技术,以避免创建闭包对象。然而委员会中有人对此表示怀疑,质疑实现者是否会开发此类优化。提案的另一个问题是缺乏对继承(或其他行为组合机制)的支持。为此 Miller 还开发了一些提案 [Miller 2010d, 2011a],将组合性 Trait [Van Cutsem and Miller 2011c] 加入了他的类去糖化设计中。

对定义高完整性对象的支持,是委员会成员的首要任务。他们最关心的是可能试图窃取私人信息的恶意 Web 广告与 mashup。整个委员会都对此表示关切,但不一定要就此确定优先级。Waldemar Horwat [2010] 在 2010 年 9 月的 TC39 会议记录中指出:

小组内部关于目标的分歧:「高完整性」VS.「用更好的语法来支持人们已经在写的东西」VS. 也许有可能两者兼得。

Allen Wirfs-Brock 认为,如果让对象的创建变得不那么命令式,可能可以支持第二条目标。在经典的 JavaScript 中,最接近 Class 的是构造函数,它需要命令式地定义一个新对象的属性。对象字面量提供了一种更为声明式的方式来定义对象属性,但其缺乏与 ECMAScript 的内置类约定93相匹配的能力。也许对象字面量可以进行扩展,以更好地支持人们已经在写的东西,而不必引入「类」作为新的语言实体。

  1. function tripleFactory(a, b, c) {
  2. return { // 这个对象字面量用于创建 triple 对象
  3. <proto: Array.prototype, // 由 proto 元属性设置继承的原型
  4. sealed>, // 用 Object.seal() 封住元属性
  5. 0: a,
  6. 1: b,
  7. 2: c,
  8. var length const: 3, // var 会设置 [[enumerable]] 为 false
  9. // const 会设置 [[writable]] 为 false
  10. method toString() { // 方法是有函数值的数据属性
  11. // 并且其 [[ enumerable ]] 为 false
  12. return "triple(" + this[0] + "," + this[1] + "," + this[2] + ")"
  13. },
  14. method sum(){ return this[0] + this[1] + this[2] }
  15. }
  16. }

图 45. 基于 Wirfs-Brock 的 Harmony 扩展对象字面量提案的工厂函数。

在一组相关提案中,Wirfs-Brock [2011c; 2011d] 展示了如何扩展对象字面量,使其更为声明式,并消除在定义常规对象时使用 ES5 对象反射 API 的需求。例如,图 45 显示了在基于扩展对象字面量的工厂函数g时,该如何定义具有显式原型、方法和私有属性的类。

Allen Wirfs-Brock 的提案还展示了对于扩展对象字面量的语法,该如何将其用作类定义的主体。在 2011 年 3 月的 TC39 演讲中 Wirfs-Brock [2011a] 提出,类定义应该能生成 ECMAScript 规范第 15 条94里内置库 Class 所使用的「构造函数、原型对象和实例对象」基本三要素,这在所有 ECMA-262 已有版本中都是通用的。与其将类定义去糖化为 lambda 表达式(糖化类)或一种新的运行时实体(受 Java 启发的类),不如将其去糖化为 JavaScript 程序员和框架作者们已经使用且熟悉的构造函数和原型继承对象。在会议上,大家对扩展对象字面量语法的许多细节有很大的意见分歧,但达成了一个宽松的共识,即核心类定义的语义,应该符合规范第 15 条中的构造函数、原型、实例三要素。

2011 年 5 月初,TC39 的 ES.next 特性冻结会议迅速临近,此时仍然有几个与类相关的稻草人提案在进行竞争。看起来委员会仍然未必有足够的共识,能使其中的某个提案被采纳。2011 年 5 月 10 日,Allen Wirfs-Brock 与 Mark Miller、Peter Hallam 和 Bob Nystrom 见了面。Hallam 和 Nystrom 是使用 Google 的 Traceur 转译器 [Traceur Project 2011b],对 JavaScript 类支持进行原型设计的团队成员。他们的原型融合了 Wirfs-Brock 和 Miller 提案中的想法。会议的目标是取得足够的一致意见,以便能提出一份统一的提案。Bob Nystrom [2011] 在其会议报告中列出了许多一致意见,包括:

……构造函数、原型和实例这三要素,足以解决其他语言中的类所要解决的问题。Harmony 类语法的目的,并不是去要改变这些语义。相反地,它是要为这些语义提供一种简明而声明式的外表,以便体现程序员的意图,而非底层的命令式机制。

……对象是声明式和信息性的,函数则是命令式和行为式的。类的问题在于:「我们是否应将其建立在这些抽象的基础上。如果是的话,应该选择哪一个?」……

在我们的共识提案中,会通过结合这两种手段来解决这种宗教式的分歧:引入一种类似对象字面量的形式作为类体,再加上一个函数来作为构造器。

会后,Mark Miller [2011b] 创建了一份新的稻草人提案。尽管该提案中仍有许多细节缺乏共识,它在特性冻结会议 [TC39 2011b] 上仍然获得了接受。图 46 中作为示例的类定义,是基于 Miller 的特性冻结类提案而给出的:

  1. class Monster extends Character {
  2. constructor(name, health) { // 构造器函数
  3. super(); // 调用父类构造器
  4. public name = name; // 公有实例属性
  5. private health = health; // 私有实例变量
  6. }
  7. attack(target) { // 原型方法
  8. log('The monster attacks ' + target);
  9. }
  10. get isAlive() { // 原型 get 访问器
  11. return private(this).health > 0;
  12. }
  13. set health(value) { // 原型 set 访问器
  14. if (value < 0) {
  15. throw new Error('Health must be non-negative.')
  16. }
  17. private(this).health = value
  18. }
  19. public numAttacks = 0; // 原型数据属性
  20. public const attackMessage = 'The monster hits you!'; // 只读
  21. }

图 46. 基于 Mark Miller [2011b] 统一化 Harmony Class 提案的类。

一个月后,Dave Herman [2011c] 在一篇题为「最小化的类」的 es-discuss 帖子中,对 class 提案的复杂性及其诸多分歧点给 ES.next 带来的时间风险表示了担忧。他提出了另一种最小化的设计,它只包含:带原型继承的类声明、构造器、声明式方法,并使用 super 关键字调用被继承的方法。被排除的是声明式属性、构造器属性、私有数据,以及其他任何有争议的内容。Herman 的建议在 2011 年 7 月的会议 [TC39 2011a] 上进行了讨论,但委员会决定将重点放在解决当时 Mark Miller 提案中的未决问题上。Brendan Eich [2012a] 后来写道:

去年夏天在 Redmond,最小化类有了一个很好的 TC39 支持子集。但我们当时卡在对「const 和 guard 使用前初始化的未来前景」的讨论上……

关于类的替代性设计 [Ashkenas 2011; Eich 2011a; Herman 2011a] 的持续在线讨论,促使 Dave Herman [2011d] 写了一份新的「最小类」稻草人提案。这份提案将 Herman 之前的帖子形式化,但增加了「静态」构造器数据和方法属性。在接下来的两次 TC39 会议上,几乎没有对 Herman 的最小化提案所进行的讨论,在解决计划中分歧的方面也没有什么进展。Brendan Eich [2012c] 对这个问题的描述如下:

……Waldemar 观察到的总体趋势是真实的:如果(提案的覆盖面)太小,就没有意义。而如果太大,我们又很难同意。我们需要「金发姑娘」(童话《金发姑娘和三字小熊》中的主人公,译者注)——恰到好处的温度和数量。

到 2012 年 3 月初,es-discuss 社区成员对于 TC39 明显无法完成 ES.next 中类的设计,表示出了越来越大的失望。Russell Leggett [2012] 在一篇题为「为类找到一个『安全』语法」95的文章中提出了这个问题:

我们是否能想出一种大家都认为「比没有好」的类语法,并注重于为将来的改进留出可能性呢?作为一种「安全语法」,这并不意味着我们停止尝试寻找更好的语法。它只意味着如果我们还没有找到答案,那我们也仍然留着一些东西——这些东西我们可以在 ES7 中做得更好。

Leggett 的帖子在三天内收到了 119 个以正面为主的回复。它列出了一套「绝对最低的要求」,这与 Dave Herman 去年夏天的清单基本相同。Leggett 的贡献是创造了「安全学校」的隐喻。Allen Wirfs-Brook 对此立即表示支持,并创造了一份新的「最大化的最小」(max-min)版本 [Wirfs-Brock 2012d] 提案,用这个隐喻重新定义了 Herman 的最小化类提案。这里最大的技术变动,是移除了原提案中的构造器属性96。如果此时要将此「max-min」提案正式列入 2012 年 3 月 TC39 会议的议程,已经为时已晚。但 Allen Wirfs-Brock 和 Alex Russell 在会议结束时,领导了一次非正式讨论 [TC39 2012a]。总体来说,委员会对提案的接受度是积极的。但有几位成员就此表示担心,认为提案内容可能过少而不值得就此费心,或者可能会对他们考虑的未来扩展产生不利影响。当时没有试图就该提案达成共识,但 Wirfs-Brook 和 Russell 表示,任何更详细的内容都不可能进入 ES.next。

这份 max-min 提案正式列入了 2012 年 5 月的会议议程,并在会上进行了类似的讨论 [TC39 2012b],其结果是类似的。与会人员正逐步就该提案达成共识,但还有一些关键人物缺席。由于时间上的压力,与会者一致认为,已经可以就原型和初步规范草案开展工作了。到 7 月会议 [TC39 2012c] 时,Allen Wirfs-Brock 已经写好了 max-min 类提案的规范文本,并准备了一套演示文稿 [Wirfs-Brock 2012b],列举了他遇到的每项设计决策。他带领委员会逐条审查了每项决策,并记录了对某一备选方案的接受或共识。这种方法回避了就整个提案达成共识的问题,但却让委员会在细节设计层面参与了共识的形成。ES.next 规范的下一份草案 [Wirfs-Brock et al. 2012b, c] 包含了完整的 max-min 类设计,其中纳入了 7 月会议上做出的决策。对此没有人表示反对。

然而在 2014 年夏天,随着浏览器 JavaScript 引擎开发者开始实现 ES6 的类,确实出现了一条重要的反对意见。ES6 工作的长期目标之一,是提供一种「子类化」内置类的方法,如 Array [Kangax 2010] 和 Web 平台的 DOM 类。Allen Wirfs-Brock [2012c; 2012e] 写了一份 Harmony 稻草人文档,描述了为什么传统的 JavaScript 内置构造函数在进行子类化时会存在问题。内置的构造函数通常是使用语言实现所用的原生语言(如 C++)来定义的。它们会分配和初始化私有的对象表示,这些私有对象的特殊结构也会被相关的内置方法所获知,这些方法也是用实现语言定义的。当使用 new 运算符直接调用内置构造函数时,这种方法是有效的。但当使用 JavaScript 特有的原型继承方案来「子类化」这样的构造函数时,new 运算符会被应用于子类构造函数(通常用 JavaScript 编码)上。它所分配出的是一个普通对象,而不是被继承的内置方法所期望的私有对象表示。Wirfs-Brock [2013] 在确定 max-min 类的语义时,试图避免这个问题。new 的语义被分割成了单独的分配阶段和初始化阶段。对象分配是由 new 首先调用一个特别命名的 @@create 方法来进行的。该方法通常由内置的父类提供,而不会被子类覆盖。对象初始化发生在分配之后,与子类的构造函数相协调。它通常会对其父类构造函数进行 super 调用,以执行所有特定于父类的必要初始化,然后再执行所有特定于子类的必要初始化。如果编码得当,这可以使内置的父类在将对象传递给子类构造函数之前,分配出其特殊的私有对象结构。子类构造函数可以使用其初始化代码,将子类属性添加到父类提供的对象中。

2014 年发现的问题在于,@@create 方法创建的对象是未初始化的。某个错误或恶意的类构造函数,可能会在未初始化的对象上调用内置的父类方法(很可能由 C++ 实现)——这可能导致灾难性的后果。Wirfs-Brock 曾假设所有这类对象都会在内部跟踪它们的初始化状态,并且需要相应的内置方法,来检查它们是否被应用了到一个未初始化的对象上。Mozilla 的 Boris Zbarsky [2014] 指出,浏览器中有数千种这样的方法,而在区分两阶段的设计中,需要为每个方法更新每个浏览器的 DOM 规范和实现。这促使了单阶段分配 / 初始化设计 [Wirfs-Brock et al. 2014c, d] 和另一份提案 [Herman and Katz 2014] 的发展。这份提案保留了两个阶段,但会将构造器参数传递给 @@create 方法和构造器。在 2014 年剩余的时间里,委员会对这些方案和其他替代方案进行了激烈的辩论。在某段时间,共识的缺乏一度可能推迟原定于 2015 年 6 月发布的 ES6,甚至迫使从该版本中完全移除类。然而在 2015 年 1 月,TC39 围绕单阶段设计的变体达成了共识 [TC39 2015a; Wirfs-Brock 2015b]。这一经验再次坚定了 TC39 的决心,要求更多、更早地由实现者对 ES6 后的新特性进行反馈。

模块

ES4 设计的复杂部分之一,就是用于构建大型程序和库的「包和命名空间」结构。当新版 ES4 被放弃时,人们已经发现这些机制存在重大问题 [Dyer 2008b; Stachowiak 2008b],它们显然不适合进入 Harmony。而当时有影响力的 JavaScript 开发者们所使用的,还是基于模块模式而缺乏泛用性的模块化解决方案 [Miraglia 2007; Yahoo! Developer Network 2008]。2009 年 1 月,Kris Kowal 和 Ihab Awad 向 TC39 [2009c] 提交了一份受模块模式启发的设计 [Awad and Kowal 2009; Kowal and Awad 2009a]。他们的设计最终演变成了 Node.js 中使用的 CommonJS 模块系统。

Kris Kowal 和 Ihab Awad 在他们最初的提案和随后的修订版 [Kowal 2009b; Kowal and Awad 2009b] 中,纳入了一些语法糖式的替代方案。这些方案可能会覆盖他们的模块设计,而不会改变提案的动态语义。Awad [2010a; 2010c] 随后开发了一份不同的提案,这份提案借鉴了 CommonJS 上的工作,以及 E 语言 [Miller et al. 2019] 的 Emaker 模块。这些 Emaker 模块正被与安全 ECMAScript 相关的 Caja 项目 [2012] 所使用。在 TC39 内部,这些提案被称为「一等公民式模块系统」,因为它们将模块表现为动态构造出的一等公民式运行时实体,这提供了一种新的计算抽象机制。例如在 Awad 的提案中,一个模块的多个实例可能同时存在,每个实例用不同的参数值初始化。

Brendan Eich [2009c] 描述了一种替代方法:

Harmony 中的替代方案是一种特殊的语法形式。比如说 import 指令,它可以在程序解析(而非执行)时进行分析。这样语言实现可以在执行前预先加载好所有的依赖关系,以免在导入(或出现延迟的数据依赖)时阻塞。否则就要使用一种较不方便的非阻塞导入,以保留 JS「运行到完成」的执行模式。

这种替代方案被称为「静态」或「二等公民式模块」系统。这种模块系统提供了使应用代码结构化的机制,而非定义出新的计算抽象机制。对此 Sam Tobin-Hochstadt [2010] 解释说:

……在一个有状态的语言中,你会希望能在不改变其行为的情况下,将程序划分成模块。对于有一段有状态的代码,在你把它移到自己的模块中后,所造成的影响不应该多于任何其他重构。如果你需要反复创建新状态,ES 也提供了不错的机制。同样地,如果有一个导入了 A 的模块,你可以把它拆分成两个都导入了 A 的新模块。像这种重构也不应该改变程序的工作方式。

Dave Herman 和 Sam Tobin-Hochstadt 为二等公民式 Harmony 模块开发了「简单模块」设计 [Herman 2010b, c, f; Herman and Tobin-Hochstadt 2011; Tobin-Hochstadt and Herman 2010],其基本思想在于将模块视作「可共享词法绑定的代码单元」。新语法将用于划分出代码单元,并确定出哪些绑定将被共享。在 Awad [2010b] 建议 TC39 将工作重点放在 Herman 和 Tobin-Hochstadt 的提案上之前,TC39 对这两种方法的优点进行了广泛的讨论。

他们的设计中具有 module 声明,其中会为模块分配一个词法标识符。这要么会引入模块代码,要么会确定包含相应代码的外部资源。而对于具备 export 关键字前缀的声明,其绑定将被暴露到模块外部。例如:

  1. // 最早的 Harmony 简单模块提案
  2. module m1 { // 一个内部模块
  3. export var x = 0, y=0;
  4. export function f() {/* ... */};
  5. }
  6. module m2 { // 同个源文件内的另一个内部模块
  7. export const pi = 3.1415926;
  8. }
  9. // 用于确定外部模块的字符串字面量
  10. module mx = load "http://example.com/js/x.js";
  11. // ... 后续代码可导入并使用来自 m1,m2 和 mx 中的绑定

模块声明也可以进行嵌套。一个形如 x.js 的外部模块,可以只包含一个模块主体,而不必以模块声明语法包围它。import 声明用于使某个模块所导出的绑定,能在词法上被导入它的模块所访问。使用上述示例模块的代码,可能会有如下的 import

  1. // 最早的 Harmony 简单模块提案
  2. import m1.{x, f}; // 从 m1 导入两个绑定
  3. import m2.{pi: PI}; // 导入一个绑定并重命名,以便于本地访问
  4. import mx.*; // 导入所有由 mx 导出的绑定
  5. import mx as X; // 将 X 本地绑定到以 mx 导出字段为属性的对象

通过模块声明、字符串字面量形式的外部模块标识,以及声明式的导出 / 导入定义,可以静态地确定一组由相互依赖的模块组成的封闭集合。这些模块之间的共享词法绑定,可以在执行代码之前进行链接。循环依赖也是允许的。当执行开始时,模块会按照规定好的确定性顺序进行初始化。如果有任何无法初始化的循环依赖关系,TDZ 死区会确保抛出运行时错误。

模块语法发生了演变 [Herman et al. 2013],但「模块具备共享词法绑定,且可静态链接」这一基本思想仍然存在。主要的语法变化之一,是取消了显式的模块声明语法、模块标识符,以及内部 / 嵌套模块。每个源文件对应一个 Harmony 模块,其中使用字面量形式的字符串资源标识符来进行识别。模块标识符的取消,需要改变 import 语法。另外通配符导入也被取消,因为它太容易出错。通配符导入被替换成了另一种形式,这种形式会将一组开放式的导入指令暴露为「单一命名空间下的对象属性」,而非作为单独的词法绑定。对于前述中的 import 示例,其基于最终版语法的表达是这样的:

  1. // ES2015
  2. import { x, f } from "m1.js"; // 从 m1 导入两个被导出的绑定
  3. import { pi as PI } from "m2.js"; // 导入一个绑定并重命名,以便于本地访问
  4. import * as X from "mx.js"; // 将 X 本地绑定到命名空间对象,其属性映射为 mx.js 所导出的字段
  5. // 新增的导入形式
  6. import "my.js"; // 仅为初始化副作用而导入 my.js
  7. import z from "mz.js"; // 导入由 mz.js 所导出的唯一默认绑定

module 声明的取消和默认绑定 import 形式的增加,均属于设计的后期变化。Node.js 的普及出乎意料地迅速,它将 CommonJS 模块广泛暴露在了 JavaScript 开发者社区中。TC39 为此收到了负面的社区反馈 [Denicola 2014],并担心 CommonJS 模块事实上的标准化,可能会给 Harmony 设计蒙上阴影。TC39 为此增加了 export default 形式,以适应那些习惯于在许多 CommonJS 模块中使用单体导出设计模式97的开发者。TC39 模块倡导者们也开始向 Node.js 开发者布道 [Katz 2014] Harmony 模块。

最初的「简单模块」提案包含了模块加载器 [Herman 2010e] 的概念,它提供了将模块整合到运行中的 JavaScript 程序时的语义。其目的在于由 ECMAScript 规范来定义出:模块的语言级语法和语义、模块加载的运行时语义,以及模块加载器的 API。这其中模块加载器的 API,能为 JavaScript 程序员提供「控制和扩展加载器语义」的机制。加载过程最终被设想 [Herman 2013b] 为一条由五个阶段组成的流水线,包括规范化、解析、获取、翻译和链接。加载器首先会对模块标识符进行规范化处理。然后它会通过对模块源码的检索和预处理,确定模块的相互依赖性,将导入和导出联系起来,最后再初始化相互依赖的模块。模块加载器的设计目标是高度的灵活性,以完全支持 Web 浏览器的异步 I/O 模式。在 2011 年的 JSConf 上,Dave Herman 展示了 [Leung 2011] 一个概念验证性的模块加载器。它扩展了加载过程中的翻译阶段,将 CoffeeScript 和 Scheme 代码加载为了运行在 JavaScript 网页之中的模块。

为了充分理解模块加载过程和该如何确定它,Dave Herman 与 Mozilla 的 Jason Orendorff 合作,使用 JavaScript 代码实现了一个模块加载器参考实现的原型 [Orendorff and Herman 2014]。2013 年 12 月,Herman [2013a] 完成了对 Orendorff 的 JavaScript 代码的初步改写,使其变成了规范伪代码。2014 年 1 月,Allen Wirfs-Brock [2014a] 将伪代码初步整合到了 ES6 草案中。但 Wirfs-Brock 发现模块加载器的异步性质,给 ECMAScript 规范增加了新的复杂性和潜在的不确定性。这种情况因加载器 API 而变得更糟,因为它允许用户程序在模块加载过程中注入任意的 JavaScript 代码。到 2014 年年中,异步模块加载的额外复杂性和 API 中一连串难以解决的设计问题,似乎已经危及了 ES6 在 2015 发布版本的目标。

在开发简单模块提案的早期阶段,Allen Wirfs-Brook [2010] 曾注意到模块作用域和链接的语义,可以从加载器管道中分离出来。在之前的 ECMA-262 版本中,规范已经定义了 JavaScript 源码的语法和语义,但并未涉及该如何访问它。这是由托管 JavaScript 引擎的环境来承担的责任。在 2014 年 9 月的 TC39 会议 [TC39 2014b] 上,Wirfs-Brock 认为类似的方法也可以适用于模块,这样 ECMA-262 就不需要包含模块加载管道的规范了。如果 ECMA-262 假定模块的源码都已经存在,那么只要规定各独立模块的语法和语义,以及该如何「将被导入和导出的绑定联系起来」的语义就足够了。浏览器等宿主环境可以提供异步加载管道,但其定义将与语言规范解耦。要移除加载器管道,也意味着要移除加载器 API。TC39 接受了这一观点。Wirfs-Brook 也得以在 2014 年 10 月的规范草案 [Wirfs-Brock et al. 2014b] 中,纳入了基本完整的语言级模块规范。模块语义与加载器管道的分离,使得 WHATWG 能够专注于确定 ECMAScript 模块该如何与 Web 平台 [Denicola 2016] 集成。

箭头函数

ES2015 引入了一种简洁的函数定义表达形式,通常称之为「箭头函数」。箭头函数的写法是以形参列表为起始,然后是 => 标记和函数体。例如:

  1. (a, b) => { return a + b }

如果只有一个形参,那么可以省略括号。而如果函数体是单条 return 语句,还可以省略括号和 return 关键字。例如:

  1. x => x /* 一个 identity 函数 */

与其他函数定义形式不同的是,箭头函数不会重新绑定 this 和其他函数作用域内的隐式绑定。这使得箭头函数在「内层函数需访问其外层函数的隐式绑定」的情况下,显得非常方便。

设计箭头函数的主要动机,在于开发者经常需要编写冗长的函数表达式,以此作为平台和库 API 函数的回调参数。在 JavaScript 1.8 中,Mozilla [2008b] 实现了98「表达式闭包」,它保留了对function 关键字的使用,允许使用无括号的单个表达式体。TC39 讨论了一些类似但较短小的表示法,用诸如 𝜆、f、\ 或 # 等符号 [Eich 2010b; TC39 Harmony 2010c] 来代替函数,但未能就其中任何一种方法达成共识。

TC39 [Herman 2008] 同时也对提供具有精简语义的「lambda 函数」感兴趣,比如支持消栈的尾调用g和 Tennent [1981] 一致性原则99。其支持者们认为,这样的函数将会在实现由语言或库所定义的控制抽象时有所用处。在 Harmony 进程早期,Brendan Eich [2008a] 在 es-discuss 上的一篇讨论贴中, 提出了一个最初由 Allen Wirfs-Brook 所提出的建议,即基于 Smalltalk 块语法的启发,采用一种简洁的 lambda 函数语法。例如 {|a,b| a+b} 就相当于 Herman 的 lambda(a,b){a+b}。Eich 的帖子引发了一场大规模但没有结论的线上讨论,话题涉及与(某种可能的)简明函数特性所相关的方方面面。作为关键总结,可以认为其中许多语法的灵感会带来解析或可用性上的问题,而且 JavaScript 的非本地控制转移语句——returnbreakcontinue——会显著地使编写控制抽象的机制变得更加复杂。大多数 TC39 成员和 es-discuss 订阅者似乎主要对简洁的函数语法更感兴趣,而非对 Tennent 一致性感兴趣。

在这之后的 30 个月里,这方面都没有出现什么重大进展,直到 Brendan Eich [2011f; 2011g] 撰写了两份替代性的稻草人提案为止。这两份提案之中,有一份设计的是「箭头函数」,它参照了 CoffeeScript 中的类似特性。这份提案中有 ->=> 两种函数,它们具备各种语法和语义上的差异和选项。而另一份提案设计的,则是以 Smalltalk 和 Ruby 的块为模型的「块级 lambda」,它还支持 Tennent 一致性。在随后的 9 个月里,这两项提案及其备选方案在 es-discuss 和 TC39 会议上得到了广泛的讨论。有人担心如果要支持解析箭头函数,现有的 JavaScript 实现是否易于更新。这里的问题是箭头符号出现在整个结构的中间,而且它前面还有一个形参列表,因此可能会被有歧义地解析为括号表达式。对于块级 lambda 提案,有人担心 [Wirfs-Brock 2012a] 它所创建出的用户定义控制结构,并不能充分而完整地与内置的语法控制结构相集成。Brendan Eich 总体倾向于块级 lambda 提案,但随着 2012 年 3 月 TC39 会议的临近,他认为箭头函数更有可能被委员会接受。在会议上 [TC39 2012a],他向委员会介绍了一套关于箭头函数最终设计基本特征的共识性决定 [Eich 2012b]。

其他特性

除上述已经讨论过的内容外,重要的新语言特性还包括如下:

  • 对象字面量的增强,包括计算属性名和简洁的方法语法。
  • 在对象与数组的初始化声明和赋值运算符中使用解构。
  • 形式参数增强,包括剩余参数、可选参数默认值,以及参数解构。
  • 受 Python 启发的迭代器和生成器,但与其有显著的不同。
  • for-of 语句,以及在新场景和改进后的场景下普遍使用的迭代器协议。
  • 在字符串和正则表达式中支持完整的 Unicode。
  • 支持嵌入领域特定语言(domain specific language)的模板字面量。
  • 作为属性键使用的 Symbol 值。
  • 二进制和八进制数字字面量。
  • 消栈的尾调用100

语言内置库的增强包括:

  • 新的 Array 方法。
  • offrom 构造器方法约定,用于创建数组和其他集合对象。
  • 类型数组类,包括用于操作二进制数据的 DataViewArrayBuffer。它们都基于 Khronos Group [2011] 规范中之前实现出的浏览器宿主对象,但与语言的其他部分有了更好的集成。类型数组现在还支持了大多数的 Array 方法。
  • MapSet 这类具有键的集合,以及 WeakMapWeakSet
  • 额外的 MathNumber 函数。
  • 用于复制对象属性的 Object.assign 函数。
  • 用于延迟访问异步计算值的 Promise 类。
  • 反映内部元对象协议的 Reflect 函数。

延期和被放弃的特性

在 ES6 的开发过程中,TC39 还考虑了许多稻草人特性提案,但它们最终没有被纳入为 ES2015 的特性。这其中许多提案在最初提出后不久就被拒绝,但其他一些则曾属于重要的开发工作,有些甚至在最终被从版本中移除之前,已经推进到了成为被接受的 Harmony 提案的程度。在被削减掉的内容中,有一些提案被放弃,另一些则被推迟,以便开展更多的工作,并可能考虑纳入未来的版本中。截至 ES2015 完成前不久,被削减的重要特性和开发工作主要包括以下内容:

  • 推导式 [Herman 2010a, d, 2014a; TC39 2014a] 推导式原本可提供一种更简洁而声明式的方式,来创建出一个初始化后的数组,或定义出一个生成器函数。它基于 Python 和 JavaScript 1.7/1.8 中的类似特性。
  • 模块加载器 API [Herman 2013b] 模块加载器 API 原本可让 JavaScript 程序员动态介入模块加载器的处理过程。程序可能会使用该 API 来完成一些处理,比如在加载过程中插入一个转译器,或支持模块的动态定义。这个 API 和模块加载器一起被推迟。
  • Realms API [Herman 2014b] Realm API 原本可使 JavaScript 程序员能在新的 Realms 中创建、补充和执行代码,它与模块加载器 API 密切相关。这一特性被推迟,以进行额外的设计工作。
  • 模式匹配 [Herman 2011e; Rossberg 2013] 解构的通用化,它原本将包括受 Haskell 启发的可驳式匹配(refutable matching)。
  • Object.observe [Arvidsson 2015; Klein 2015; Weinstein 2012] 一种复杂的数据绑定机制,可以在受监控对象的属性被修改时产生事件。
  • 并行 JavaScript [Hudson 2012, 2014] 又名 River Trail,是英特尔和 Mozilla 的一个联合项目,旨在使 JavaScript 程序员能够明确地利用处理器的 SIMD 能力。
  • 值对象 [Eich 2013] 其目标是提供一种通用性的支持,以便定义出类似 Number 和 String 的新原始数据类型(包括运算符重载)。这可以允许库实现十进制小数、大整数等特性。
  • Guards [Miller 2010c] 为声明添加的类似于类型的注解,可对其进行动态验证。