最近看到一个关于js中的let关键字帖子:JavaScript let 关键字问题求解 ,说的是下面一段代码:
function clickImageIcon(msgArr, options) {
for (var i = 0; i < msgArr.length; i ++) {
$('.file-wrapper:eq(' + i + ')').bind('click', function () {
recognitionContent(msgArr[i]);
$('#myModal').modal(options);
});
}
}
代码的目的是使用循环为多个元素帮点点击事件,但是测试发现并没有按照预想的那样正确的为元素绑定事件。为什么呢?
别急,先来看这段代码
var a = true;
if(a){
var b = 1;
}
console.log(b)
在Chrome网页中的console中运行这段js代码,你猜运行结果是啥?答案是1。
作为一个从事C系语言编程的人,看到这个结果还是蛮惊异的,原来你竟是这样的var——原因其实也很简单,js中var 是函数级的,无视内部大括号。
更深层的原因是缘于JavaScript引擎的工作方式:先解析代码,获取所有被声明的变量,然后再一行一行地运行。这造成的结果,就是所有的变量的声明语句,都会被提升到代码的头部,这就叫做变量提升(hoisting)。
用闭包可以解决本文开头代码的问题:
function clickImageIcon(msgArr, options) {
for (var i = 0; i < msgArr.length; i ++) {
(function(index) {
$('.file-wrapper:eq(' + index + ')').bind('click', function () {
recognitionContent(msgArr[index]);
$('#myModal').modal(options);
});
})(i);
}
}
解释:由于运行时闭包的存在,该匿名函数中定义的变量(包括参数表)在它内部匿名函数执行完毕之前都不会释放,因此我们在其中访问到的 i 就分别是不同的闭包实例,这个实例是在循环体执行的过程中创建的,保留了不同的值。
但是有一个很巧妙的解决办法是把var改成let:
function clickImageIcon(msgArr, options) {
for (let i = 0; i < msgArr.length; i ++) {
$('.file-wrapper:eq(' + i + ')').bind('click', function () {
recognitionContent(msgArr[i]);
$('#myModal').modal(options);
});
}
}
这是为啥呢?这就要提到ES6中引入的let的特点了:
- let声明的变量拥有块级作用域。
也就是说用let声明的变量的作用域只是外层块,而不是整个外层函数。let 声明仍然保留了提升特性,但不会盲目提升,在示例一中,通过将var替换为let可以快速修复问题,如果你处处使用let进行声明,就不会遇到类似的bug。 - let声明的全局变量不是全局对象的属性。
这就意味着,你不可以通过window.变量名的方式访问这些变量。它们只存在于一个不可见的块的作用域中,这个块理论上是Web页面中运行的所有JS代码的外层块。 - 形如for (let x...)的循环在每次迭代时都为x创建新的绑定。
这是一个非常微妙的区别,拿示例二来说,如果一个for (let...)循环执行多次并且循环保持了一个闭包,那么每个闭包将捕捉一个循环变量的不同值作为副本,而不是所有闭包都捕捉循环变量的同一个值。
所以示例中,也以通过将var替换为let修复bug。
这种情况适用于现有的三种循环方式:for-of、for-in、以及传统的用分号分隔的类C循环。
用let重定义变量会抛出一个语法错误(SyntaxError)。这个很好理解,跟C系语言变量定义一致。
等一等——形如for (let x...)的循环在每次迭代时都为x创建新的绑定——这句话怎么看都有点疑惑,为啥会这样?C/Objective-C中的变量作用域虽然并没有像var那样不堪,但也没有for循环中的这个let效果,你们不好奇它是怎么做到的吗?
我们来换一段代码试试:
for (let i = 0 /* 作用域a */; i < 3; console.log("in for expression", i), i++) {
let i; //你猜这里会不会报错?
console.log("in for block", i);
}
运行一下,你会发现并不报错。运行结果是:
in for block undefined
in for expression 0
in for block undefined
in for expression 1
in for block undefined
in for expression 2
好像真是天大的发现呢,大括号里的let i不会报错,说明大括号里的let作用域并不是for循环的let子作用域?
我们来看看把上面的代码进行ES6 to ES5转换后的代码:
for (var i = 0 /* 作用域a */; i < 3; console.log("in for expression", i), i++) {
var i = void 0; //你猜这里会不会报错?
var s;
console.log("in for block", i);
}
是不是大失所望,只是简单地把let换成var而已?非也,这只是因为内部没有异步调用。
现在我们往for循环里加入一个异步调用的延时函数:
for (let i = 0 /* 作用域a */; i < 3; console.log("in for expression", i), i++) {
let i; //你猜这里会不会报错?
setTimeout(function(){console.log(i)})
}
转换为ES5的结果是:
var _loop = function _loop(i) {
var i = void 0; //你猜这里会不会报错?
setTimeout(function () {
console.log(i);
});
};
for (var i = 0 /* 作用域a */; i < 3; console.log("in for expression", i), i++) {
_loop(i);
}
可以看到for循环大括号的内容被提取到一个单独的_loop函数里去了。
简单的理解就是:
- 所有的let声明最终还是var声明
- 带let声明的for循环中如果有异步调用,则大括号内容最终会提取为一个单独的函数
对于本文第二段代码,如果把var b = 1改成let b = 1,进行ES6 to ES5转换后的代码:
var a = true;
if (a) {
var _b = 1;
}
console.log(b);
只不过是很简单的变量重命名。
至于const,则是更特殊的let——即不可修改的let变量声明。
参考: