首先我们来看这样一段代码:
for (var i=0; i<3; i++) {
setTimeout(() => {
console.log(i)
}, 0)
}
很多刷过面试题的jser看到这段代码应该很熟悉了,脱口就能说出正确答案是 3 个 3,但有不少人却不明白其中的原理。别小看这段代码,其中暗含了不少的js的基础知识。
从代码层面来,很多童鞋会想当然的将上述代码理解成以下的内容:
for (var i=0; i<3; i++) {
console.log(i)
}
即,输出为0、1、2。
如果这样想的话,他们至少犯了2个错误:
- 写出第一段代码的童鞋,都是试图假设循环中的每个迭代在运行时都会给自己“捕获”一个 i 的副本。但是根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个 i 。
- setTimeout等延迟函数的回调会在循环结束时才会执行,故而不管循环多少次,延迟多少时间,console.log中的i,必然是共享的全局作用域中变量i,在此例中就是3。
从第一段代码的执行顺序来看,其演示代码应该是:
var i=0;
i++;
i++;
i++;
console.log(i);
console.log(i);
console.log(i);
上述代码就很好地解释了第一段代码3个3的执行结果了。
~~ 那怎样才能使上述代码的输出为0、1、2呢?
提供两种方法:
- 使用let
for (let i=0; i<3; i++) {
setTimeout(() => {
console.log(i)
}, 0)
}
let 本质上这是将一段代码块转换成一个可以被关闭的作用域。或许这样看会比较直观。
for (var i=0; i<5; i++) {
let j = i
setTimeout(() => {
console.log(j)
}, 0)
}
let的存在,会产生一个隐形的作用域,使得每次循环的 i 值通过复制给隐形作用域内的 j 保存下来。
- 使用function创建独立作用域
for (var i=0; i<3; i++) {
(function(j) {
setTimeout(() => {
console.log(j)
}, 0)
})(i)
}
众所周知,js 中 function 可以提供一段独立的作用域,那我们就可以通过在迭代内使用function会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。