为语言添加新特性的最佳方法是什么?本章介绍 ECMAScript 6 所采用的方法。它被称为 *One JavaScript*,因为它避免了版本控制。
let
声明原则上,新版本的语言是通过删除过时的特性或更改特性的工作方式来清理语言的机会。这意味着新代码不能在旧版本的语言实现中工作,而旧代码也不能在新版本的实现中工作。每段代码都链接到特定版本的语言。处理版本差异有两种常见方法。
首先,您可以采用“要么全有,要么全无”的方法,并要求如果代码库要使用新版本,则必须完全升级。 Python 从 Python 2 升级到 Python 3 时就采用了这种方法。它的一个问题是,一次迁移所有现有代码库可能不可行,尤其是在代码库很大的情况下。此外,该方法不适用于 Web,因为 Web 上始终存在旧代码,并且 JavaScript 引擎会自动更新。
其次,您可以通过使用版本标记代码来允许代码库包含多个版本的代码。在 Web 上,您可以通过专用的 Internet 媒体类型 标记 ECMAScript 6 代码。可以通过 HTTP 标头将此类媒体类型与文件相关联
Content-Type: application/ecmascript;version=6
它也可以通过 <script>
元素的 type
属性关联(其 默认值 为 text/javascript
)
<
script
type
=
"application/ecmascript;version=6"
>
···
</
script
>
这指定了 *带外* 版本,即在实际内容之外指定。另一种选择是在内容内部指定版本(*带内*)。例如,通过以下行开始文件
use
version
6
;
两种标记方式都存在问题:带外版本很脆弱,容易丢失,带内版本会增加代码的混乱程度。
更根本的问题是,允许每个代码库使用多个版本实际上是将一种语言分叉成必须并行维护的子语言。这会导致问题
因此,版本控制是要避免的,尤其是对于 JavaScript 和 Web 而言。
但是,我们如何才能摆脱版本控制呢?通过始终保持向后兼容性。这意味着我们必须放弃一些清理 JavaScript 的雄心壮志:我们不能引入破坏性变更。向后兼容意味着不删除功能也不更改功能。该原则的口号是:“不要破坏 Web”。
但是,我们可以添加新功能并使现有功能更强大。
因此,新引擎不需要版本,因为它们仍然可以运行所有旧代码。David Herman 将这种避免版本控制的方法称为 *One JavaScript* (1JS) [1],因为它避免了将 JavaScript 拆分为不同的版本或模式。正如我们稍后将看到的,1JS 甚至撤消了一些由于严格模式而已经存在的拆分。
One JavaScript 并不意味着您必须完全放弃清理语言。您可以引入新的、干净的特性,而不是清理现有特性。一个例子是 let
,它声明块级作用域变量,是 var
的改进版本。但是,它不会取代 var
。它与之并存,作为更好的选择。
总有一天,甚至可以消除不再有人使用的功能。一些 ES6 特性是通过调查 Web 上的 JavaScript 代码而设计的。两个例子是
let
声明很难添加到非严格模式中,因为 let
在该模式下不是保留字。唯一看起来像有效的 ES5 代码的 let
变体是
let
[
x
]
=
arr
;
研究表明,Web 上没有任何代码以这种方式在非严格模式下使用变量 let
。这使得 TC39 能够将 let
添加到非严格模式。有关如何完成此操作的详细信息,请参阅本章后面的内容。
严格模式 是在 ECMAScript 5 中引入的,用于清理语言。通过将以下行放在文件或函数的第一行来启用它
'use strict'
;
严格模式引入了三种破坏性变更
with
语句。它允许用户将任意对象添加到变量作用域链中,这会降低执行速度,并使得难以确定变量引用的是什么。implements interface let package private protected public static yield
ReferenceError
。在非严格模式下,在这种情况下会创建一个全局变量。TypeError
。在非严格模式下,它根本不起作用。arguments
不再跟踪参数的当前值。this
为 undefined
。在非严格模式下,它指的是全局对象(window
),这意味着如果您在没有 new
的情况下调用构造函数,则会创建全局变量。严格模式是版本控制为何棘手的一个很好的例子:即使它启用了更干净的 JavaScript 版本,但它的采用率仍然相对较低。主要原因是它会破坏一些现有代码,可能会降低执行速度,并且添加到文件(更不用说交互式命令行)很麻烦。我喜欢严格模式的 *想法*,但并没有经常使用它。
One JavaScript 意味着我们不能放弃松散模式:它将继续存在(例如,在 HTML 属性中)。因此,我们不能在严格模式的基础上构建 ECMAScript 6,我们必须将其功能添加到严格模式和非严格模式(又称松散模式)中。否则,严格模式将是该语言的不同版本,我们将回到版本控制。不幸的是,有两个 ECMAScript 6 特性很难添加到松散模式中:let
声明和块级函数声明。让我们研究一下原因以及如何添加它们。
let
声明 let
使您能够声明块级作用域变量。将其添加到松散模式很困难,因为 let
仅在严格模式下是保留字。也就是说,以下两条语句是合法的 ES5 松散代码
var
let
=
[];
let
[
x
]
=
'abc'
;
在严格的 ECMAScript 6 中,您会在第 1 行中收到异常,因为您正在使用保留字 let
作为变量名。第 2 行中的语句被解释为 let
变量声明(使用解构)。
在松散的 ECMAScript 6 中,第一行不会导致异常,但第二行仍然被解释为 let
声明。这种使用标识符 let
的方式在 Web 上非常罕见,以至于 ES6 可以负担得起这种解释。其他编写 let
声明的方式不会被误认为是松散的 ES5 语法
let
foo
=
123
;
let
{
x
,
y
}
=
computeCoordinates
();
ECMAScript 5 严格模式禁止在块中进行函数声明。该规范允许在松散模式下使用它们,但没有指定它们的行为方式。因此,JavaScript 的各种实现都支持它们,但处理方式不同。
ECMAScript 6 希望块中的函数声明对该块是局部的。作为 ES5 严格模式的扩展,这是可以的,但是会破坏一些松散的代码。因此,ES6 为浏览器提供了“Web 旧版兼容性语义”,使块中的函数声明可以存在于函数作用域中。
标识符 yield
和 static
仅在 ES5 严格模式下保留。ECMAScript 6 使用上下文相关的语法规则使它们在松散模式下工作
yield
仅在生成器函数内部是保留字。static
当前仅在类文字内部使用,而类文字是隐式严格的(请参见下文)。在 ECMAScript 6 中,模块和类的正文隐式处于严格模式 - 不需要 'use strict'
标记。鉴于我们几乎所有代码将来都将驻留在模块中,因此 ECMAScript 6 有效地将整个语言升级到了严格模式。
其他构造的正文(例如箭头函数和生成器函数)也可以隐式地设置为严格模式。但是考虑到这些构造通常很小,因此在松散模式下使用它们会导致代码在两种模式之间 fragmentation。类,尤其是模块,都足够大,可以减少 fragmentation 的问题。
One JavaScript 的缺点是您无法修复现有的怪癖,尤其是以下两个。
首先,typeof null
应该返回字符串 'null'
而不是 'object'
。TC39 尝试修复它,但它破坏了现有代码。另一方面,为新类型的操作数添加新结果是可以的,因为当前的 JavaScript 引擎有时已经为宿主对象返回自定义值。一个例子是 ECMAScript 6 的符号
> typeof Symbol.iterator
'symbol'
其次,全局对象(浏览器中的 window
)不应位于变量的作用域链中。但是现在更改它也为时已晚。至少,在模块中,一个变量不会处于全局作用域中,并且 let
永远不会创建全局对象的属性,即使在全局作用域中使用时也是如此。
ECMAScript 6 确实引入了一些小的破坏性变更(您不太可能遇到)。它们列在两个附件中
一个 JavaScript 意味着使 ECMAScript 6 完全向后兼容。令人高兴的是,这已经成功了。特别值得赞赏的是,模块(以及我们的大部分代码)默认处于严格模式。
在短期内,在编写语言规范和在引擎中实现它时,将 ES6 结构添加到严格模式和非严格模式都需要做更多的工作。从长远来看,规范和引擎都将从语言没有分叉(更少的膨胀等)中获益。程序员可以立即从一个 JavaScript 中获益,因为它使 ECMAScript 6 的入门变得更容易。
[1] 最初的 1JS 提案(警告:已过时):David Herman 的“ES6 不需要选择加入”。