es6 in depth let 与 const
当Brendan Eich在1995年设计了第一版javascript, 他留下了很多错误, 包括至今还留着的部分, 包括Dateobject和object会在你试图乘他们的时候转换为NaN. 但他也有做得好的地方: object; prototypes; 等等. 让语言有了骨架. 使语言比看上去的更好.
当然Brendan也做了一些设计导致了今天文章的主题~让我们来看一下吧.
是关于变量的.
问题#1: 代码块没有作用域
这个规则听上去没什么错: js函数中的var关键字创建的作用域是这个函数的整个函数体. 但有两个情况会产生问题.
一个问题是在代码块中声明的变量作用不是代码块, 而是整个函数.
你可能之前都没注意过. 恐怕这个问题你不可以当做没看见一样. 我们来谈谈这个问题会导致bug的场景吧.
假设你在代码中用了变量t:
1 | function runTowerExperiment(tower, startTime) { |
目前看起来一切都正常. 现在你像加一个测试保龄球测速器, 所以你需要写一些if语句在回调方法中.
1 | function runTowerExperiment(tower, startTime) { |
哦, 亲爱的. 你不经意地写了第二个变量t. 之前”用了变量t的代码”工作正常, 而现在t指向的是代码块内部的变量而不是外部的t了.
var在javascript中就像把变量扔进了染缸. 会向两边拓展定义, 前和后, 直到方法边界. 虽然变量t的作用域拓展到方法头部, 但还是在创建时进入方法的. 这个行为被成为变量提升. js引擎会把每个var和function声明的变量提升到函数块的头部.
变量提升有他的好处. 许多写得好的代码不适用立即执行函数. 但在这个case中, 变量提升导致了很麻烦的问题: 你所有使用t变量的地方会开始产生NaN. 并且很难去追踪. 特别是在更大的项目中.
新加一个代码块会产生莫名其妙的错误, 我们并不想代码产生额外的行为.
这只是var问题的一部分.
问题#2: 循环中变量指向
你可以猜一下下面代码的运行结果, 这很简单:
1 | var messages = ["Hi!", "I'm a web page!", "alert() is fun!"]; |
如果你一直追这个es6系列的文章, 你会发现我一直用alert(). 也许你知道alert()是个很可怕的api, 他是同步的. 所以当alert弹出的时候, 输入事件不会被传递, 你的js代码 — 事实上是你整个UI — 在你点击确定前全被暂停了.
在你的web页面中使用alert()是不好的, 但我用她是觉得alert()是一个很好的测试工具.
接下来我要写个说话的猫的代码:
1 | var messages = ["Meow!", "I'm a talking cat!", "Callbacks are fun!"]; |
但是好像有问题, 猫没有说那些话, 而是说了3次”undefined”.
你可以定位到bug吗?

这里的问题就出在变量i. 循环的变量共享了外面的变量, 当循环结束, i的值是3, 所以messages[3]是undefined.
let是新的var
大部分情况, javascript的设计错误(别的语言也如此, 但特别是js)不能被修复. 因为需要兼容之前的代码. 即使标准委员会也没权利说来修复 javascript中奇怪的自动补分号的行为. 浏览器也不会去实现断层的更新, 这样会影响到用户.
所以大概10年前, Brendan Eich决定要修复这个问题, 而且只有一个办法可以做到.
他加了一个新关键字let, 用来定义变量, 和var用法一样, 但是有更好的作用域规则.
看起来是这样的:
1 | let t = readTachymeter(); |
或者是:
1 | for (let i = 0; i < messages.length; i++) { |
let和var是不同的, 如果你进行全局替换, 会破坏你的代码(可能是不经意的), 因为var奇怪的行为. 但在大多数的情况中, 在用es6新写的代码中, 你应该要在任何情况下停止使用var而使用let来替代. 因此才有了这个口号: “let是新的var“.
那么到底let和var有什么区别呢? 很高兴你这么问.
let变量是块级作用域的.let定义的变量作用域是当前代码块, 而不是函数块.let仍然有变量提升, 但不是盲目的了. 刚才的runTowerExperiment的例子可以通过简单地用let替代var来修复. 如果你到处都用let那就不会有这种问题了.全局中使用
let不会把变量挂到全局object上. 也就是说你不能通过window.variableName来拿到变量了. 这些变量现在在一个看不见的抽象的闭包中.类似
for(letx...)的循环每次遍历都会创建一个新的x.这是一个很微小的变化. 意思是
for(let...)循环执行了多次, 循环会维护一个闭包, 比如刚才说话的猫的例子, 每次循环都会捕捉当前循环的变量的副本, 而不像但作用域一样捕捉到了相同的变量.如果在定义
let变量前就使用会报错. 直到变量被声明前, 变量都没有被初始化. 看例子:1
2
3
4
5function update() {
console.log("current time:", t); // ReferenceError
...
let t = readTachymeter();
}这个规则是帮你查错的. 如果这么写会直接报错, 而不是得到一个
NaN.这种情况: 变量在一个作用域内, 但没被初始化, 这个区域被称作暂时的死区. 这里会去等到变量被声明的地方为止.
重新声明
let会导致SymtaxError.这个规则也是用来帮我们检查错误的. 这也是如果你把
let换成var以后很容易发生的错误, 即使let是全局变量也如此.如果你在多个脚本中都使用了全局变量, 你最好用
var来代替他. 如果你使用let, 那么这些脚本加载时会报错.或者使用es6的modules. 这是以后讲的故事了.
除了这些区别,
let和var是一样的. 他们都支持用逗号分隔声明多个变量, 也都支持解构赋值.
注意class的声明行为类似let. 所以如果你写了多个class, 第二次相同名字就会报重新定义的错.
const
好~ 再来一个!
es6还提供了第三个关键字: const.
被const修饰的变量行为与let一样, 除了: 在声明以外的地方为变量赋值都会得到SyntaxError.
1 | const MAX_CAT_SIZE_KG = 3000; // 🙀 |
自然地, 如果你不能不给任何值地声明一个const.
1 | const theFairest; // SyntaxError, you troublemaker |
(本文完)
如果你可以 点击这个链接打赏我5毛来鼓励我, 非常感谢.
本文遵循 cc协议
你可以在注明出处和非商用的前提下任意复制及演绎
