💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
# 3. 一个JavaScript:在 ECMAScript 6 中避免版本化 > 本章地址:https://exploringjs.com/es6/ch_one-javascript.html 给一门语言添加新特性的最好方式是什么?本章描述了 ECMAScript 6 使用的方式。它被称作 \*一个 JavaScript \*,因为避免了版本化。 ## 3.1 版本化 原则上,语言的一个新版本是一个清理的机会,可以清理过时的特性或者改变特性的工作方式。这意味着新的代码在语言的旧的实现中无法工作,老的代码在新的实现中无法工作。每段代码都和特定的语言版本关联。针对两个不同语言版本写两种不同代码是很常见的。 首先,你可以使用一种“用所有还是什么都不用”的方式:如果一个代码库需要使用新的语言版本,就必须彻底升级。Python 在从 Python 2 升级到 Python 3 的时候就是这样的。这样的话有个问题,将已有的代码库一次性全部升级可能是做不到的,尤其是代码库很大的时候。而且,这种方式对于 web 来说是不可行的,因为总是有老代码,而且 JavaScript 引擎会自动升级。 第二,你可以让一个代码库包含在多个语言版本都可运行的代码,通过根据版本切换代码的方式。你可以通过一个专用的[互联网媒体类型](http://en.wikipedia.org/wiki/Internet_media_type)标记 ECMAScript 6 的代码。这样的媒体类型可以和一个 HTTP 头关联在一起: ``` Content-Type: application/ecmascript;version=6 ``` 也可以和`<script>`元素中的`type`属性关联在一起(`type`[默认值](http://www.w3.org/TR/html5/scripting-1.html#attr-script-type)是`text/javascript`): ```html <script type="application/ecmascript;version=6"> ··· </script> ``` 这是在代码之外指定版本。另一种方案是在代码内指定版本。例如,将下面的代码放在 JavaScript 文件的第一行: ~~~js use version 6; ~~~ 两种标记的方法都很容易产生问题:外部版本标记法很脆弱,容易丢失;内部版本标记法又会使代码显得杂乱。 一个更根本的问题是,针对不同的语言版本,要维护不同的执行引擎。这就引起了一些问题: * 引擎变得臃肿,因为要实现所有版本的语义。对于分析语言的工具也带来了同样的问题(比如类型检测, JSLint )。 * 开发者需要记住版本之间的不同点。 * 代码变得更加难以重构,因为在移动代码的时候需要考虑语言版本的问题。 因此,应该避免版本化,尤其是 JavaScript 和 web 。 ### 3.1.1 非版本化的升级 但是我们如何解决版本化的问题?通过一直向后兼容。这意味着我们必须放弃一些野心,比如清理 JavaScript :我们不能引入破坏性改变。向后兼容意味着不要移除特性,也不要修改特性。原则是:“不要破坏 web ”。 然而,我们可以添加新的特性,并且使已有的特性更加强大。 因此,对于新的引擎不需要版本化管理了,因为仍然可以运行老的代码。 David Herman 称这种避免版本化的方法为[`一个 JavaScript ( One JavaScript(1JS) [1] )`](http://exploringjs.com/es6/ch_one-javascript.html#one-js_1),因为它使 JavaScript 避免分成几种不同的版本或模式。正如我们将要看到的,由于严格模式,使得 1JS 甚至移除了一些已有的分裂。 一个 JavaScript ( One JavaScript )不是说你必须完全放弃语言清理。你可以引入新的干净的特性,而不是清理掉已有的特性。其中一个例子就是`let`,它声明了块级范围的变量,是`var`的一个升级版本。它并不取代`var`,而是作为高级可选项一直和`var`并存。 某一天,甚至可能清除掉没人使用的特性。一些 ES6 特性是在调查过 web 上的 JavaScript 代码才设计的。举两个例子: * `let`声明很难添加到非严格模式,因为`let`在这种模式下是保留字。使用`let`关键字看起来像是合法 ES5 代码的形式是: ```js let[x] = arr; ``` 经研究得出,在 web 上没人以这种方式在非严格模式下使用变量`let`。这使得 TC39 将`let`添加进了严格模式。在本章后面讲述了这是如何做的。 * 函数声明确实偶尔会在非严格模式下的代码块中出现,这就是为什么 ES6 规范描述了 web 浏览器可以采用的措施来确保这样的代码不会被破坏。后面详细讲解([3.2.3 松散模式下的块级函数声明](#))。 ## 3.2 严格模式与 ECMAScript 6 ECMAScript 5 引入 [Strict mode](http://speakingjs.com/es5/ch07.html#strict_mode)来清理语言,在文件或者函数的第一行放入下面的内容就可以开启严格模式: ```js 'use strict'; ``` 严格模式引入了三种破坏性的改变: * 语法改变:一些之前合法的语法在严格模式下面是不允许的。例如: * 禁止`with`语句。它允许使用者添加任何对象到变量作用域链,这会减缓程序的执行速度,并且很难指出某个变量指向哪里。 * 删除一个独立的标识符(一个变量,而不是一个属性)是不允许的。 * 函数只能在作用域的顶层声明。 * 更多的保留字: implements interface let package private protected public static yield 。 * 更多种类的错误。例如: * 给一个未声明的变量赋值会抛出`ReferenceError`。在非严格模式下,这样干就会创建一个全局变量。 * 修改只读的属性(比如字符串的长度属性)会抛出`TypeError`。在非严格模式下,不会产生任何效果。 * 不同的语义:在严格模式下,一些语法结构表现得不一样。例如: * `arguments`不再随着当前参数值的改变而改变。 * 在非方法的函数中`this`是`undefined`。在非严格模式下,它指向全局对象(`window`),也就是说如果调用一个构造器的时候没有使用`new`,就会创建一些全局变量。 严格模式是一个很好地说明了版本化是棘手的:即便能够制作一个干净版本的 JavaScript ,也很难被大家接受。主要原因是破坏了一些现存的代码,降低了执行速度,并且加入到文件中也很麻烦(更不用说交互的命令行)。我喜欢严格模式这种想法,但却很少使用它。 ### 3.2.1 支持松散( sloppy 非严格)模式 一个 JavaScript (One JavaScript) 意味着我们不能放弃松散模式:此模式将会继续存在(例如在 HTML 属性中)。因此,我们不能基于严格模式来构建 ECMAScript 6 ,必须同时在严格模式和非严格模式(又称为松散模式)下添加特性。否则,严格模式就会成为一个不同的语言版本,就退回了版本化的方式。很不幸,有两个特性很难引入松散模式:`let`声明和块级函数声明。让我们看看为什么很难引入和如何引入。 ### 3.2.2 松散模式中的`let`声明 `let`使你能够声明块级变量。这很难被引入到松散模式,因为`let`仅在严格模式下是保留字。也就是说,下面两条语句在 ES5 的松散模式下是合法的: ```js var let = []; let[x] = 'abc'; ``` 在 ECMASCript 6 的严格模式下,第一行就会抛出异常。因为使用了`let`作为变量名。然后第二行会被解析为一个`let`变量声明(使用解构)。 在 ECMAScript 6 的松散模式下,第一行不会抛出异常,但是第二行依然被解析为一个`let`声明。这种使用`let`的方式在 web 上是极少见的,因此 ES6 可以直接这样来解析。 ES5 松散模式下的其他`let`声明的书写方式不会被误解: ```js let foo = 123; let {x,y} = computeCoordinates(); ``` ### 3.2.3 松散模式下的块级函数声明 ECMAScript 5 严格模式禁止在块中声明函数,在松散模式下,规范却允许这么做,但是没说这样会发生什么。因此,很多 JavaScript 实现都支持块级函数声明,但是处理方式是不一样的。 ECMAScript 6 想要块中的函数声明本地化(即该函数的作用域救在该块中)。作为 ES5 严格模式的扩展,这是没问题的,但是破坏了一些松散模式的代码。因此, ES6 为浏览器提供了“[web 遗留的兼容语义](http://www.ecma-international.org/ecma-262/6.0/#sec-block-level-function-declarations-web-legacy-compatibility-semantics)”,允许块中的函数声明在函数作用域中存在。 ### 3.2.4 其它关键字 标识符`yield`和`static`仅在 ES5 的严格模式下是保留字。 ECMAScript 6 使用上下文相关的语法规则来使它们在松散模式下起作用: * 在松散模式下,`yield`仅在生成器函数中是保留字。 * `static`现在仅用于类字面量中,类字面中默认就是严格的(见下文)。 ### 3.2.5 **隐式的严格模式** 在 ECMAScript 6 中,模块体和类体 默认就是严格模式的–没必要使用`use strict`标记。考虑到我们所有的代码都将会位于模块中, ECMAScript 6 有效地将整个语言升级到了严格模式。 其它结构体(比如箭头函数和生成器函数)本来也应该隐式地为严格模式。但是考虑到通常情况下这些结构都很小,在非严格模式下使用它们就会造成代码中两种模式的碎片化切换。类,尤其是模块一般是足够大的,这样一来就可以忽略碎片化的代码片段问题了。 > 那么需要我们去留心 严格模式 下的一些约束,会发生什么变化。 ### 3.2.6 无法修复的东西 一个 JavaScript 的缺陷(The downside of One JavaScript)就是无法修复已有的怪异行为,尤其是下面这两个。 第一个,`typeof null`应该返回字符串`null`而不是`object`。但是修正这个就会破坏已有的代码。另一方面,给新种类的操作数添加新的操作结果是没问题的,因为当前的 JavaScript 引擎对于一些宿主对象已经会返回自定义的值。 ECMAScript 6 的 Symbol 就是一个例子: ```js > typeof Symbol.iterator 'symbol' ``` 第二个,全局对象(浏览器中的`window`对象)不应该在变量作用域链。但是现在修正这个也太晚了。但是至少,在模块中不会处于全局作用域下,并且`let`永远不会创建全局对象属性,甚至在全局作用域下使用也不会。 ``` let letStr = 'letStr'; console.log(window.letStr) // undefined var varStr = 'varStr'; console.log(window.varStr) // “varStr” ``` ## 3.3 ES6中的突破性变化(Breaking changes) ECMAScript 6确实引入了一些微小的突破性变化(你可能不会遇到)。它们列在两个附件(annexes)中: - [附件D:2015年电子手册中可能对兼容性产生影响的更正和澄清](http://www.ecma-international.org/ecma-262/6.0/#sec-corrections-and-clarifications-in-ecmascript-2015-with-possible-compatibility-impact) - [附件E:引入与以前版本不兼容的添加和更改](http://www.ecma-international.org/ecma-262/6.0/#sec-additions-and-changes-that-introduce-incompatibilities-with-prior-editions) ## 3.4 总结 一个 JavaScript 意思就是使 ECMAScript 6 完全地向后兼容,很高兴这获得了成功。尤其是模块隐式就是严格模式的(这样一来我们大部分的代码都会处于严格模式下)。 在短期内,对于制定 ES6 规范和引擎实现来说,给严格模式和松散模式添加 ES6 的语法结构会耗费更多的精力。从长远来看,规范和引擎将会受益于语言不分叉(更少的膨胀等等)。开发人员会立即从一个 JavaScript 中获得好处,因为开始使用 ECMAScript 6 变得更加容易。 ## 3.5 深入阅读 [1] 原始的 1 JS 提案(警告:已过时): “[ES6 doesn’t need opt-in](http://esdiscuss.org/topic/es6-doesn-t-need-opt-in)”,作者 David Herman 。