当初Brendan Eich发明JavaScript的时候,只用了10天,比上帝创造人类差不了多少。在如此短的时间内创造一门新的语言,设计上的缺陷无可避免(当然其他语言或多或少都会有)。其中一个就是声明变量的关键字var
。
var
用于声明一个变量,该变量的作用范围是整个函数范围内,或者是全局的。例如如下语句:
// case 1
function func() {
var v = 'hello world';
if (true) {
// do something
}
console.log(v);
}
// case 2
function func() {
if (true) {
// do something
var v = 'hello world';
}
console.log(v);
}
从结果上,以上两个函数的运行结果是完全一致的。因为存在吊升(hoisting),不管var
在哪里声明一个变量,该变量声明都会优先于其他代码被处理,因此在声明前引用该变量是完全没问题的,不会引发任何异常,只不过结果不是我们所预期的。不过值得一提的是,虽然存在hoisting,但是变量真正赋值依旧是要等待代码执行到它所在的位置。例如上面的例子case 2中,如果在声明变量v
之前使用它,会得到undefined
。
正是由于这个特性的存在,可能会引发一些意想不到的问题。请看大屏幕:
class clazz {
show() {
console.log('hello there!');
}
}
function func() {
var v = new clazz();
if (true) {
// 一不小心再次定义的 v
var v = 'oh no...'
}
v.show();
}
此时,程序已经无法给你想要的结果。
再举个栗子:
var messages = ["Am", "I", "a", "bug?"];
for (var i = 0; i < messages.length; i++) {
setTimeout(function () {
console.log(messages[i]);
}, i * 1500);
}
我们的初衷是延迟1.5秒后分别依次输出Am I a bug?这四个单词。但实际结果却是四个undefined
。为什么会这样?注意for
循环中var
声明的变量i
,实际上,由于是var
声明的,所以整个循环中只有一个i
,这同一个i
被for
循环以及三个回调函数共用。因此当循环结束时,其实i=4
,而messages[4]
显然已经超出了原本数组的范围,因此显示undefined
。那么我们要怎么解决这个问题呢?好了,为了解决这个问题,关键先生登场,它就是let
关键字。只要把上面的for
循环中var
换成let
,问题就解决了。
let
是何方神圣,它是谁?它从哪里来?它能干什么?这些问题很好回答,简单一句话:let
是新的var
。
十年前,JavaScript的作者Brendan Eich打算修复var
存在的毛病(例如上面所举的例子),但是由于要兼容历史遗留的代码,这些缺陷一旦存在就几乎不可能再被修复,毕竟让已经在网上存在已久的一大堆代码瞬间出毛病,是个开发者都要拿起菜刀拼命。因此,要修复var
的缺陷只有一个办法,原封不动的保留var
的语义,而引入另外一个新的关键字作为它的替代,这个关键字就是let
。
那么究竟他们俩有什么不同呢?莫急,听我慢慢道来。
let
的作用范围只在它声明的代码块内,也就是说,出了声明的代码块再去引用它就属于违规了,这一点就和java
等类似了;而var
声明的变量,var
定义的变量是全局的,或者是在整个函数中生效的,不管他在那里被声明的,都会先于其他任何语句被处理,这个称之为“吊装(hoisting)”。虽然let
也存在hoisting,但是并没有被滥用,它只被吊升到声明该变量的代码块顶部,而不是整个函数的顶部。
function varTest() {
var x = 1;
if (true) {
var x = 2; // same variable!
console.log(x); // 2
}
console.log(x); // 2
}
function letTest() {
let x = 1;
if (true) {
let x = 2; // different variable
console.log(x); // 2
}
console.log(x); // 1
}
let
定义的的全局变量并不是全局对象的一个属性,因此你并不能通过window.variableName
的方式去访问一个let
定义的变量variableName
;- 在
let
声明的变量在吊升时不会被初始化成默认值;var
声明的变量会初始化成undefined
; for (let i=0, ...)
的形式在每次迭代的时候都会创造一个新的变量i
的副本,并不是所有迭代共享一个。因此可以解决我们上面的例子存在的问题。var
声明的变量可以在再次被声明,但是在let
声明的变量的函数或者是代码块内再次声明该变量会抛出SyntaxError
。如:
let x = 1;
switch(x) {
case 0:
let foo;
break;
case 1:
let foo; // SyntaxError for redeclaration.
break;
}
- 对于
var
声明的变量,声明之前的地方使用得到的值是undefined
,因为吊升(hoisting)的时候给他初始换成undefined
。但是let
声明的变量虽然也有吊升,但却不会被初始化,直到执行声明的地方才会被初始化成明确的值,因此在声明的地方之前使用let
声明的变量会引发ReferenceError
。在代码块开始到let
声明的变量中间的这段,称之为“时间死区(temporal dead zone)”。 - 在“时间死区”内使用typeof,对于
let
声明的变量,会抛出ReferenceError
。如:
// prints out 'undefined'
console.log(typeof undeclaredVariable);
// results in a 'ReferenceError'
console.log(typeof i);
let i = 10;
可以说,能用var
的地方都可以用let
来代替。当然这是指新开发的情况下,对于历史遗留的代码,你并不能通过简单的查找替换全部的var
来解决,因为可能你的代码的正常运行,正是依赖的var
的一些古怪的特性。let出现,对于既有代码已经于事无补,它的作用是让你今后开发的时候,能够尽量远离一些var
引入的一些稀奇古怪的坑。
以上就是var
和let
相爱相杀的故事。
bonus time
用于定义变量的关键字其实还有另外一个,const
,它定义的变量基本和let一样,唯一不同的,就是它的变量不能被再次赋值,也即是说,过了过了这村儿没这店儿,你只有在声明的时候顺路赋值那一次机会。
本文首发于个人公众号TensorBoy。如果你觉得内容还不错,欢迎分享并关注我的公众号TensorBoy,扫描下方二维码获取更多精彩原创内容!