es6 in depth let 与 const
当Brendan Eich在1995年设计了第一版javascript, 他留下了很多错误, 包括至今还留着的部分, 包括Date
object和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协议
你可以在注明出处和非商用的前提下任意复制及演绎