[关闭]
@lenville 2015-11-02T09:43:09.000000Z 字数 5681 阅读 564

深入浅出ES6

深入浅出ES6(十四):let和const

作者 Jason Orendorff 译者 刘振涛

译者按:ECMAScript 6已经正式发布了,作为它最重要的方言,Javascript也即将迎来语法上的重大变革,InfoQ特开设“深入浅出ES6”专栏,来看一下ES6将给我们带来哪些新内容。本专栏文章来自Mozilla Web开发者博客,由作者授权翻译并发布。

今天我想要谈论的是一个集谦逊与惊人的野心于一身的新特性。

回溯到1995年,当Brendan Eich在设计第一版JavaScript时,他搞错了许多东西,当然这也包括曾属于语言本身的一部分,例如Date对象,对象相乘被自动转换为NaN等。然而现在回过头看,语言最重要的部分都是设计合理的:对象、原型、具有词法作用域的一等函数、默认情况下的可变性等。语言的骨架非常优秀,甚至超越了人们对它的初步印象。

话说回来,正是Brendan当初的设计错误才诞生了今天这篇文章。我们这次关注的目标非常小,在你使用这门语言多年后可能根本不会注意到这个问题,但是它又如此重要,因为我们可能会误认为这个错误就是语言设计中的“the good parts”(译者注:请参考《JavaScript语言精粹》一书中附录A:毒瘤中有关作用域的描述)。

今天我们一定要把这些与变量有关的问题拿下。

问题 #1:JS没有块级作用域

请看这样一条规则:在JS函数中的var声明,其作用域是函数体的全部。乍一听没什么问题,但是如果碰到以下两种情况就不会得到令人满意的结果。

其一,在代码块内声明的变量,其作用域是整个函数作用域而不是块级作用域。

你之前可能没有关注到这一点,但我担心这个问题确实是你不能够轻易忽视的。我们一起重现一下由这个问题引发的bug。

假如你现在的代码使用了一个变量t

  1. function runTowerExperiment(tower, startTime) {
  2. var t = startTime;
  3. tower.on("tick", function () {
  4. ... 使用了变量t的代码 ...
  5. });
  6. ... 更多代码 ...
  7. }

到目前为止,一切都很顺利。现在你想添加测量保龄球速度的功能,所以你在回调函数内部添加了一个简单的if语句。

  1. function runTowerExperiment(tower, startTime) {
  2. var t = startTime;
  3. tower.on("tick", function () {
  4. ... 使用了变量t的代码 ...
  5. if (bowlingBall.altitude() <= 0) {
  6. var t = readTachymeter();
  7. ...
  8. }
  9. });
  10. ... 更多代码 ...
  11. }

哦,亲爱的,之前那段“使用了变量t的代码”运行良好,现在你无意中添加了第二个变量t,这里的t指向的是一个新的内部变量t而不是原来的外部变量。

JavaScript中var声明的作用域像是Photoshop中的油漆桶工具,从声明处开始向前后两个方向扩散,直到触及函数边界才停止扩散。你想啊,这种变量t的作用域甚广,所以一进入函数就要马上将它创建出来。这就是所谓的提升(hoisting)。变量提升就好比是,JS引擎用一个很小的代码起重机将所有var声明和function函数声明都举起到函数内的最高处。

现在看来,提升特性自有它的优点。如果没有提升的动作,许多在全局作用域范围内看似合理的完美技术在立即调用函数表达式(IIFE)中通通失效。但在上面演示的这种情况下,提升会引发令人不愉快的bug:所有使用变量t进行的计算最终的结果都是NaN。这种问题极难定位,尤其是当你的代码量远超上面这个玩具一般的示例,你会发狂到崩溃。

在原有代码块之前添加新的代码块会导致诡异的错误,这时候我就会想,到底是谁的问题,我的还是系统的?我们可不希望自己搞砸了系统。

而这个问题与接下来这个问题相比就相形见绌了。

问题 #2:循环内变量过度共享

你可以猜一下当执行以下这段代码时会发生什么,非常简单:

  1. var messages = ["嗨!", "我是一个web页面!", "alert()方法非常有趣!"];
  2. for (var i = 0; i < messages.length; i++) {
  3. alert(messages[i]);
  4. }

如果你一直跟随这个系列的文章,你知道我喜欢在示例代码中使用alert()方法。可能你也知道alert()不是一个好的API,它是一个同步方法,所以当弹出一个警告对话框时,输入事件不会触发,你的JS代码,包括你的整个UI,直到用户点击OK确认之前完全处于暂停状态。

请不要轻易使用alert()来实现web页面中的功能,我之所以在代码中使用是因为alert()特性使它变成一个非常有教学意义的工具。

而且,如果放弃所有笨重的方法和糟糕的行为就可以做出一只会说话的猫,何乐而不为呢?

  1. var messages = ["喵!", "我是一只会说话的猫!", "回调(callback)非常有趣!"];
  2. for (var i = 0; i < messages.length; i++) {
  3. setTimeout(function () {
  4. cat.say(messages[i]);
  5. }, i * 1500);
  6. }

点击查看这段代码错误的运行结果!

然而一定是哪里不对,这只会说话的猫并没有按照预期连说三条消息,它说了三次“undefined”。

你知道问题出在哪里么?

图片来源:nevil saveri

你能看到树上的毛毛虫(bug)吗?

事实上,这个问题的答案是,循环本身及三次timeout回调均共享唯一的变量i。当循环结束执行时,i的值为3(因为messages.length的值为3),此时回调尚未被触发。

所以当第一个timeout执行时,调用cat.say(messages[i]),此时i的值为3,所以猫咪最终打印出来的是messages[3]的值亦即undefined

解决这个问题有很多种方法(这里有一种),但是你想,var作用域规则接连给你添麻烦,如果能在第一时间彻底解决掉这个问题多好啊!

let是更完美的var

JavaScript的设计错误(其它语言也有,奈何JavaScript太突出)多半不能被修复。保持向后兼容性意味着永不改变JS代码在Web平台上的行为,即使连标准委员会都无权要求修复JavaScript中自动插入分号这种怪异的特性;浏览器厂商也从来不会做出突破性的改变,因为如此一来伤害的是他们的忠实用户。

所以大约十年以前,Brendan Eich决定修复这个问题,但只有唯一的解决方案。

他添加了一个新的关键词:letletvar一样,也可以用来声明变量,但它有着更好的作用域规则。

它看起来是这样的:

  1. let t = readTachymeter();

或者这样的:

  1. for (let i = 0; i < messages.length; i++) {
  2. ...
  3. }

letvar还是有不同之处的,所以如果你只是在代码中将var全局搜索替换为let,一些依赖var声明的独特特性(可能你不是故意这样写)的代码可能无法正常运行。但对于绝大多数代码来说,在ES6的新代码模式下,你应该停止使用var声明变量,能使用let就用吧!从现在起,请记住这句口号:“let是更完美的var”。

那到底letvar有什么不同呢?非常高兴你提出这个问题!

在那些不同之外,letvar几乎很相似了。举个例子,它们都支持使用逗号分隔声明多重变量,它们也都支持解构特性。

注意,class类声明的行为与var不同而与let一致。如果你加载一段包含同名类的脚本,后定义的类会抛出重定义错误。

const

是的,还有一个新的关键词!

ES6引入的第三个声明类关键词与let类似:const

const声明的变量与let声明的变量类似,它们的不同之处在于,const声明的变量只可以在声明时赋值,不可随意修改,否则会导致SyntaxError(语法错误)。

  1. const MAX_CAT_SIZE_KG = 3000; // &#x1f640;
  2. MAX_CAT_SIZE_KG = 5000; // 语法错误(SyntaxError)
  3. MAX_CAT_SIZE_KG++; // 虽然换了一种方式,但仍然会导致语法错误

当然,规范设计的足够明智,用const声明变量后必须要赋值,否则也抛出语法错误。

  1. const theFairest; // 依然是语法错误,你这个倒霉蛋

神秘的代理命名空间

“命名空间是一种绝妙的理念,我们应当多加利用!”——Tim Peters,“这是Python之禅”

嵌套作用域是编程语言背后的核心理念之一,这个理念始于大约57年前的ALGOL,现在回过头看当时的决定无比正确。

在ES3之前,JavaScript中只有全局作用域和函数作用域。(让我们忽略with语句吧。)ES3中引入了try-catch语句,意味着语言中诞生一种新的作用域,只用于catch块中的异常变量。ES5添加了用于严格的eval()方法的作用域。ES6添加了块作用域,for循环作用域,新的全局let作用域,模块作用域,以及求参数的默认值时使用的附加作用域。

所有自ES3开始添加的其它作用域非常重要,它们的加入使得JavaScript面向过程与面向对象的特性运行得犹如闭包一样平稳、精准,当然闭包也可以无缝衔接这些作用域实现各种功能。或许你在阅读这篇文章之前从未注意到这些作用域规则的存在,如果真的这样,那这门语言就恰如其分地完成了它的本职工作。

我现在可以使用let和const了么?

是的。如果要在web上使用letconst特性,你需要使用一个诸如BabelTraceurTypeScript的ES6转译器。(Babel和Traceur暂不支持临时死区特性。)

io.js支持letconst,但是只在严格模式下编码可以使用。Node.js同样支持,但是需要启用--harmony选项。

九年前,Brendan Eich在Firefox中实现了初版的let关键词。这个特性在随后的标准化进程中彻底地被重新设计了。Shu-yu Guo正在按照新标准对原有实现进行升级,该项目由Jeff Walden和其他人做代码审查。

好的,我们正处于冲刺阶段,漫长而艰难的ES6特性之旅的终点离我们不远了,成功就在眼前。两周后,我们将实现大家最期待的ES6特性(译者注:作者7月31日发文,根据推算目前应该已经完成)。下一次,我们继续拓展ES6中类的特性super,记得回来加入我们跟随Eric Faust一起《深入浅出ES6:子类》。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注