深入浅出ES6(十四):let和const
作者 Jason Orendorff github主页 https://github.com/jorendorff
回溯到1995年,当Brendan Eich在设计第一版JavaScript时,他搞错了许多东西,当然这也包括曾属于语言本身的一部分,例如Date
对象,对象相乘被自动转换为NaN等。然而现在回过头看,语言最重要的部分都是设计合理的:对象、原型、具有词法作用域的一等函数、默认情况下的可变性等。语言的骨架非常优秀,甚至超越了人们对它的初步印象。
话说回来,正是Brendan当初的设计错误才诞生了今天这篇文章。我们这次关注的目标非常小,在你使用这门语言多年后可能根本不会注意到这个问题,但是它 又如此重要,因为我们可能会误认为这个错误就是语言设计中的“the good parts”(译者注:请参考《JavaScript语言精粹》一书中附录A:毒瘤中有关作用域的描述)。
今天我们一定要把这些与变量有关的问题拿下。
问题 #1:JS没有块级作用域
请看这样一条规则:在JS函数中的var声明,其作用域是函数体的全部。乍一听没什么问题,但是如果碰到以下两种情况就不会得到令人满意的结果。
其一,在代码块内声明的变量,其作用域是整个函数作用域而不是块级作用域。
你之前可能没有关注到这一点,但我担心这个问题确实是你不能够轻易忽视的。我们一起重现一下由这个问题引发的bug。
假如你现在的代码使用了一个变量t
:
function runTowerExperiment(tower, startTime) {
var t = startTime;
tower.on("tick", function () {
... 使用了变量t的代码 ...
});
... 更多代码 ...
}
到目前为止,一切都很顺利。现在你想添加测量保龄球速度的功能,所以你在回调函数内部添加了一个简单的if
语句。
function runTowerExperiment(tower, startTime) {
var t = startTime;
tower.on("tick", function () {
... 使用了变量t的代码 ...
if (bowlingBall.altitude() <= 0) {
var t = readTachymeter();
...
}
});
... 更多代码 ...
}
哦,亲爱的,之前那段“使用了变量t的代码”运行良好,现在你无意中添加了第二个变量t
,这里的t
指向的是一个新的内部变量t
而不是原来的外部变量。
JavaScript中var
声明的作用域像是Photoshop中的油漆桶工具,从声明处开始向前后两个方向扩散,直到触及函数边界才停止扩散。你想啊,这种变量t
的作用域甚广,所以一进入函数就要马上将它创建出来。这就是所谓的提升(hoisting)。变量提升就好比是,JS引擎用一个很小的代码起重机将所有var
声明和function
函数声明都举起到函数内的最高处。
现在看来,提升特性自有它的优点。如果没有提升的动作,许多在全局作用域范围内看似合理的完美技术在立即调用函数表达式(IIFE)中通通失效。但在上面演示的这种情况下,提升会引发令人不愉快的bug:所有使用变量t
进行的计算最终的结果都是NaN
。这种问题极难定位,尤其是当你的代码量远超上面这个玩具一般的示例,你会发狂到崩溃。
在原有代码块之前添加新的代码块会导致诡异的错误,这时候我就会想,到底是谁的问题,我的还是系统的?我们可不希望自己搞砸了系统。
而这个问题与接下来这个问题相比就相形见绌了。
问题 #2:循环内变量过度共享
你可以猜一下当执行以下这段代码时会发生什么,非常简单
var messages = ["嗨!", "我是一个web页面!", "alert()方法非常有趣!"];
for (var i = 0; i < messages.length; i++) {
alert(messages[i]);
}
如果你一直跟随这个系列的文章,你知道我喜欢在示例代码中使用alert()
方法。可能你也知道alert()
不是一个好的API,它是一个同步方法,所以当弹出一个警告对话框时,输入事件不会触发,你的JS代码,包括你的整个UI,直到用户点击OK确认之前完全处于暂停状态。
请不要轻易使用alert()
来实现web页面中的功能,我之所以在代码中使用是因为alert()
特性使它变成一个非常有教学意义的工具。
而且,如果放弃所有笨重的方法和糟糕的行为就可以做出一只会说话的猫,何乐而不为呢?
var messages = ["喵!", "我是一只会说话的猫!", "回调(callback)非常有趣!"];
for (var i = 0; i < messages.length; i++) {
setTimeout(function () {
cat.say(messages[i]);
}, i * 1500);
}
然而一定是哪里不对,这只会说话的猫并没有按照预期连说三条消息,它说了三次“undefined”。
你知道问题出在哪里么?
事实上,这个问题的答案是,循环本身及三次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决定修复这个问题,但只有唯一的解决方案。
他添加了一个新的关键词:let
。let
与var
一样,也可以用来声明变量,但它有着更好的作用域规则。
它看起来是这样的:
let t = readTachymeter();
或者这样的:
for (let i = 0; i < messages.length; i++) {
...
}
let
与var
还是有不同之处的,所以如果你只是在代码中将var
全局搜索替换为let
,一些依赖var
声明的独特特性(可能你不是故意这样写)的代码可能无法正常运行。但对于绝大多数代码来说,在ES6的新代码模式下,你应该停止使用var
声明变量,能使用let
就用吧!从现在起,请记住这句口号:“let
是更完美的var
”。
那到底let
和var
有什么不同呢?非常高兴你提出这个问题!
这一规则可以帮助你捕捉bug,除了NaN
错误以外,每一个异常都会在当前行抛出。
let
声明的变量拥有块级作用域。也就是说用let
声明的变量的作用域只是外层块,而不是整个外层函数。let
声明仍然保留了提升的特性,但不会盲目提升。在runTowerExperiment
这个示例中,通过将var
替换为let
可以快速修复问题,如果你处处使用let
进行声明,就不会遇到类似的bug。let
声明的全局变量不是全局对象的属性。这就意味着,你不可 以通过window.变量名
的方式访问这些变量。它们只存在于一个不可见的块的作用域中,这个块理论上是Web页面中运行的所有JS代码的外层块。形如
for (let x...)
的循环在每次迭代时都为x创建新的绑定。这是一个非常微妙的区别,拿我们的会说话的猫的例子来说,如果一个
for (let...)
循环执行多次并且循环保持了一个闭包,那么每个闭包将捕捉一个循环变量的不同值作为副本,而不是所有闭包都捕捉循环变量的同一个值。所以在会说话的猫示例中,也可以通过将
var
替换为let
修复bug。这种情况适用于现有的三种循环方式:
for-of
、for-in
、以及传统的用分号分隔的类C循环。let
声明的变量直到控制流到达该变量被定义的代码行时才会被装载,所以在到达之前使用该变量会触发错误。举个例子:
function update() {
console.log("当前时间:", t); // 引用错误(ReferenceError)
...
let t = readTachymeter();
}
不可访问的这段时间变量一直处于作用域中,但是尚未装载,它们位于临时死区(Temporal Dead Zone,简称TDZ)中。我一直想用科幻小说来类比这个脑洞大开的行话,但是还没想好怎么搞。
(脆弱的性能细节:在大多数情况下,查看代码就可以区分声明是否已经执行,所以事实上,JavaScript引擎不需要在每次代码运行时都额外执行 一次变量可访问检查来确保变量已经被初始化。然而在闭包内部有时不是透明的,这时JavaScript引擎将会做一个运行时检查,也就意味着
let
相对var
而言比较慢。)(脆弱的平行宇宙作用域细节:在一些编程语言中,一个变量的作用域始于声明之处,而非前后覆盖整个封闭代码块。标准委员会曾考虑过将这种作用域准则赋予
let
关键词,但是一旦使用这种准则,原本提前使用变量的语句会导致引用错误(ReferenceError),现在该语句不位于let t
的声明作用域中,根本不会引用此处的变量t
,而是引用外层作用域的相应变量。但是这个方法无法与闭包和函数提升很好得结合,所以该提案最终被否决了。)用
let
重定义变量会抛出一个语法错误(SyntaxError)。这一条规则也可以帮助你检测琐碎的小问题。诚然,这亦是
var
与let
的不同之处,当你全局搜索var
替换为let
时也会导致let
重定义语法错误,因为这一规则对全局let
变量也有效。如果你的多个脚本中都声明了相同的全局变量,你最好继续用
var
声明这些变量。如果你换用了let
,后加载的脚本都会执行失败并抛出错误。或者你可以考虑使用ES6内建的模块机制,后面的文章中会详细讲解。
(脆弱的语法细节:
let
是一个严格模式下的保留词。在非严格模式下,出于向后兼容的目的,你仍可以用let
命名来声明变量、函数和参数,虽然你不会犯傻,但是你确实可以编写var let = 'q';
这样的代码!不过let let;
无论如何都是非法的。)
在那些不同之外,let
和var
几乎很相似了。举个例子,它们都支持使用逗号分隔声明多重变量,它们也都支持解构特性。
注意,class
类声明的行为与var
不同而与let
一致。如果你加载一段包含同名类的脚本,后定义的类会抛出重定义错误。
const
是的,还有一个新的关键词!
ES6引入的第三个声明类关键词与let
类似:const
。
const
声明的变量与let
声明的变量类似,它们的不同之处在于,const
声明的变量只可以在声明时赋值,不可随意修改,否则会导致SyntaxError
(语法错误)。
const MAX_CAT_SIZE_KG = 3000; // 正确 MAX_CAT_SIZE_KG = 5000; // 语法错误(SyntaxError)
MAX_CAT_SIZE_KG++; // 虽然换了一种方式,但仍然会导致语法错误
当然,规范设计的足够明智,用const
声明变量后必须要赋值,否则也抛出语法错误。
const theFairest; // 依然是语法错误,你这个倒霉蛋
神秘的代理命名空间
“命名空间是一种绝妙的理念,我们应当多加利用!”——Tim Peters,“这是Python之禅”
嵌套作用域是编程语言背后的核心理念之一,这个理念始于大约57年前的ALGOL,现在回过头看当时的决定无比正确。
在ES3之前,JavaScript中只有全局作用域和函数作用域。(让我们忽略with
语句吧。)ES3中引入了try-catch
语句,意味着语言中诞生一种新的作用域,只用于catch块中的异常变量。ES5添加了用于严格的eval()
方法的作用域。ES6添加了块作用域,for循环作用域,新的全局let
作用域,模块作用域,以及求参数的默认值时使用的附加作用域。
所 有自ES3开始添加的其它作用域非常重要,它们的加入使得JavaScript面向过程与面向对象的特性运行得犹如闭包一样平稳、精准,当然闭包也可以无 缝衔接这些作用域实现各种功能。或许你在阅读这篇文章之前从未注意到这些作用域规则的存在,如果真的这样,那这门语言就恰如其分地完成了它的本职工作。
我现在可以使用let和const了么?
是的。如果要在web上使用let
和const
特性,你需要使用一个诸如Babel、Traceur或TypeScript的ES6转译器。(Babel和Traceur暂不支持临时死区特性。)
io.js支持let
和const
,但是只在严格模式下编码可以使用。Node.js同样支持,但是需要启用--harmony
选项。
九年前,Brendan Eich在Firefox中实现了初版的let
关键词。这个特性在随后的标准化进程中彻底地被重新设计了。Shu-yu Guo正在按照新标准对原有实现进行升级,该项目由Jeff Walden和其他人做代码审查。
深入浅出ES6(十四):let和const的更多相关文章
- 深入浅出ES6(四):模板字符串
作者 Jason Orendorff github主页 https://github.com/jorendorff 反撇号(`)基础知识 ES6引入了一种新型的字符串字面量语法,我们称之为模板字符 ...
- 深入浅出ES6(十五):子类 Subclassing
作者 Jason Orendorff github主页 https://github.com/jorendorff 在之前的文章<深入浅出ES6(十三):类 Class>中,我们一起深 ...
- 深入浅出ES6(六):解构 Destructuring
作者 Jason Orendorff github主页 https://github.com/jorendorff 什么是解构赋值? 解构赋值允许你使用类似数组或对象字面量的语法将数组和对象的属性 ...
- 从壹开始前后端分离 [ Vue2.0+.NET Core2.1] 十四 ║ VUE 计划书 & 我的前后端开发简史
---新内容开始--- 番外 大家周一好呀,又是元气满满的一个周一呀!感谢大家在周一这个着急改Bug的黄金时期,抽出时间来看我的博文哈哈哈,时间真快,已经到第十四篇博文了,也很顺顺(跌跌)利利 (撞撞 ...
- webpack4 系列教程(十四):Clean Plugin and Watch Mode
作者按:因为教程所示图片使用的是 github 仓库图片,网速过慢的朋友请移步<webpack4 系列教程(十四):Clean Plugin and Watch Mode>原文地址.更欢迎 ...
- COJ966 WZJ的数据结构(负三十四)
WZJ的数据结构(负三十四) 难度级别:C: 运行时间限制:20000ms: 运行空间限制:262144KB: 代码长度限制:2000000B 试题描述 给一棵n个节点的树,请对于形如"u ...
- COJ986 WZJ的数据结构(负十四)
WZJ的数据结构(负十四) 难度级别:D: 运行时间限制:6000ms: 运行空间限制:262144KB: 代码长度限制:2000000B 试题描述 请你设计一个数据结构,完成以下功能: 给定一个大小 ...
- NeHe OpenGL教程 第二十四课:扩展
转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线 ...
- NeHe OpenGL教程 第十四课:图形字体
转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线 ...
- 深入浅出ES6(十七):展望未来
作者 Jason Orendorff github主页 https://github.com/jorendorff 出于对文章长度的考虑,我们还保留了一些尚未提及的新特性,在最后的这篇文章中我会集 ...
随机推荐
- 安装Mongodb3.0.6单实例
[root@b28-17-51 ~]#mkdir -p /export/data [root@b28-17-51 ~]#mkdir -p /export/log [root@b28-17-51 exp ...
- arcgis中求多点到一条曲线的最短欧几里得距离
1.使用的工具:Arctoolbox----Analysis Tools----Proximity----Near工具. 2.注意:在求距离之前一定要先设置好坐标系统.
- oracle 各种问题排查
一.ORA-00257 ORA-00257归档日志写满,最简单方法.可以更改归档的大小. 二.job不自动运行解决方法 http://www.cnblogs.com/xbding/p/5861443. ...
- 在VS2010 SP1基础上安装mvc3
安装VS2010 SP1后,再安装mvc3会报错,估计原因是此安装包会安装VS的补丁,而sp1的补丁版本高过此安装包的. AspNetMVC3ToolsUpdateSetup.exe 解决办法: 运行 ...
- Xcode全局断点
1.将导航器视图切换到断点导航器视图下,也可以用快捷键Command+7一步搞定,键盘是window风格的用户Command键是win键(有微软logo),然后点击左下角的+号,选择Add Symbo ...
- iOS高级编程之XML,JSON数据解析
解析的基本概念 所谓“解析”:从事先规定好的格式串中提取数据 解析的前提:提前约定好格式.数据提供方按照格式提供数据.数据获取方按照格式获取数据 iOS开发常见的解析:XML解析.JSON解析 一.X ...
- 出色的 JavaScript API 设计秘诀
设计是一个很普遍的概念,一般是可以理解为为即将做的某件事先形成一个计划或框架. (牛津英语词典)中,设计是一种将艺术,体系,硬件或者更多的东西编织到一块的主线.软件设计,特别是作为软件设计的次类的AP ...
- [2016-06-28]dhclient命令的进程没杀死,导致不断在向DHCP服务器获取IP
# Date:2016-06-28 # 问题:主机的配置文件/etc/sysconfig/network-scripts/ifcfg-eth0 已经配置好了静态的IP. 但隔几分钟主机的IP就自己变化 ...
- 仿照CREATE_FUNC实现CCLayer中的返回CCScene* 的静态函数,宏包装成CREATE_SCENE(XXLayer)
#define CREATE_SCENE(__TYPE__)\ CCScene *scene()\ { CCScene *scene=CCScene::create();\ __TYPE__ *lay ...
- “我爱淘”第二冲刺阶段Scrum站立会议1
完成任务: 完成了webservice的配置与测试,实现了在客户端的搜索功能,并且可以实现图书的发布功能,就是将图书的信息添加到数据库中. 计划任务: 在客户端实现分类功能,通过学院的分类查看书籍. ...