最新发布《数智时代的AI人才粮仓模型解读白皮书(2024版)》,立即领取! 了解详情
写点什么

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

  • 2015-11-04
  • 本文字数:5057 字

    阅读完需:约 17 分钟

编者按: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

复制代码
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”。

你知道问题出在哪里么?

你能看到树上的毛毛虫(bug)吗?(图片来源: nevil saveri

事实上,这个问题的答案是,循环本身及三次 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一样,也可以用来声明变量,但它有着更好的作用域规则。

它看起来是这样的:

let t = readTachymeter();或者这样的:

复制代码
for (let i = 0; i < messages.length; i++) {
...
}

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

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

这一规则可以帮助你捕捉 bug,除了NaN错误以外,每一个异常都会在当前行抛出。

  • **let声明的变量拥有块级作用域。** 也就是说用let声明的变量的作用域只是外层块,而不是整个外层函数。

    let声明仍然保留了提升的特性,但不会盲目提升。在runTowerExperiment这个示例中,通过将var替换为let可以快速修复问题,如果你处处使用let进行声明,就不会遇到类似的 bug。

  • **let声明的全局变量不是全局对象的属性。** 这就意味着,你不可 以通过window. 变量名的方式访问这些变量。它们只存在于一个不可见的块的作用域中,这个块理论上是 Web 页面中运行的所有 JS 代码的外层块。

  • 形如for (let x...)的循环在每次迭代时都为 x 创建新的绑定。

    这是一个非常微妙的区别,拿我们的会说话的猫的例子来说,如果一个for (let...)循环执行多次并且循环保持了一个闭包,那么每个闭包将捕捉一个循环变量的不同值作为副本,而不是所有闭包都捕捉循环变量的同一个值。

    所以在会说话的猫示例中,也可以通过将var替换为let修复 bug。

    这种情况适用于现有的三种循环方式:for-offor-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)。

    这一条规则也可以帮助你检测琐碎的小问题。诚然,这亦是varlet的不同之处,当你全局搜索var替换为let时也会导致let重定义语法错误,因为这一规则对全局let变量也有效。

    如果你的多个脚本中都声明了相同的全局变量,你最好继续用var声明这些变量。如果你换用了let,后加载的脚本都会执行失败并抛出错误。

    或者你可以考虑使用 ES6 内建的模块机制,后面的文章中会详细讲解。

    (脆弱的语法细节:let是一个严格模式下的保留词。在非严格模式下,出于向后兼容的目的,你仍可以用let命名来声明变量、函数和参数,虽然你不会犯傻,但是你确实可以编写var let = 'q';这样的代码!不过let let;无论如何都是非法的。)

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

注意, 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 上使用letconst特性,你需要使用一个诸如 Babel Traceur TypeScript 的 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:子类》。

2015-11-04 04:4347412
用户头像

发布了 63 篇内容, 共 131.7 次阅读, 收获喜欢 38 次。

关注

评论

发布
暂无评论
发现更多内容

第5章-《Linux一学就会》- Linux基本操作和服务器硬件选购指南

学神来啦

Linux linux运维 linux学习 Linux教程

国庆临近,字节后端开发3+4面,终于拿到秋招第一个offer

Java 架构 面试 后端 计算机

音视频编解码 --X264码率控制初探

Fenngton

音视频 ffmpeg 编码 码率控制 引航计划

浅谈语音质量保障:如何测试 RTC 中的音频质量?

阿里云视频云

阿里云 测试 WebRTC 语音 音频

如何使用ESD二极管,设计运算放大器电压保护?

不脱发的程序猿

电路设计 ESD二极管 运算放大器 电压保护 嵌入式硬件

如何撬动企业数字化转型?智能客服是关键支点

ToB行业头条

Linux用户/用户组编辑

在即

9月日更

2021金九银十,啃完这35个Java技术栈,冲刺百万年薪不是梦

Java 架构 面试 程序人生 编程语言

🏆【SpringBoot技术专题】「FtpServer文件服务」教你如何基于Springboot开发一个”可移植“的轻量级文件服务项目系统!

洛神灬殇

Apache springboot ftp服务 9月日更 FtpServer

刷爆Leetcode!字节算法大佬进阶专属算法笔记:GitHub标星97k+

Java 架构 面试 程序人生 LeetCode

金九银十面试如何得到面试官青睐?2021最新大厂Java面试真题合集(附权威答案)

Java 架构 面试 程序人生 编程语言

垂直CRM,能否走到终局?

ToB行业头条

论文阅读丨神经清洁: 神经网络中的后门攻击识别与缓解

华为云开发者联盟

神经网络 深度学习 论文阅读

GK架构营模4作业

Ping

云原生时代,如何构建数字化转型架构?

博文视点Broadview

让数据库无惧灾难,华为云GaussDB同城双集群高可用方案正式发布

华为云开发者联盟

数据库 高可用 集群 华为云GaussDB 同城双集群

阿里内部最新“SpringCloudAlibaba学习笔记”(全彩第三版)限时开源

Java 架构 面试 微服务 Alibaba

应用开发中的存储架构进化史——从起步到起飞

Java 编程 架构 面试 后端

解密秒杀系统架构,不是所有的系统都能做秒杀!

华为云开发者联盟

架构 秒杀 电商系统

金秋国庆|官微掌门人火热征集!期待你的掌舵!

InfoQ写作社区官方

国庆中秋 热门活动

智云盾捕获多个僵尸网络利用最新ConfluenceRCE漏洞的活动

百度开发者中心

安全 漏洞

三款Linux文件传输工具简单介绍-行云管家

行云管家

Linux 文件传输 IT运维

如何管理职场新人?

石云升

团队管理 管理 引航计划 内容合集 9月日更

HarmonyOS荣膺2021世界互联网大会领先科技成果奖

Geek_283163

华为 鸿蒙

遇见乌镇 | VoneCredit洞见供应链金融新未来

旺链科技

世界互联网大会 供应链金融

PerfDog携手Imagination,助力开发者获取GPU关键数据

WeTest

AI专家一席谈:复用算法、模型、案例,AI Gallery带你快速上手应用开发

华为云开发者联盟

算法 模型 案例 AI Gallery 应用开发

等保测评一次多少钱,收费标准是怎样的?

行云管家

网络安全 等保 等保测评 等保2.0

ThingMap一键城市2.0重新出发:快速生成三维城市

ThingJS数字孪生引擎

地图 物联网 可视化 数字孪生

【大咖直播】Elastic 企业搜索实战工作坊(第一期)

腾讯云大数据

elasticsearch

一文带你掌握工作流引擎flowable所有业务概念

小鲍侃java

后端 引航计划

深入浅出ES6(十四):let和const_JavaScript_Jason Orendorff_InfoQ精选文章