首先来看一个常见的问题:
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
正常情况下,我们期望这段代码以每秒一个的频率分别输出数字1~5。但实际上,这段代码在运行时会以每秒一次的频率输出五次6。
那问题来了,为什么以每秒一次的频率输出五次6,而不是我们期望的1到5呢?
首先,我们都知道在ES6出现之前,js的作用域只有两种:顶层作用域和函数作用域。所以我们在for循环头部定义的变量 i 其实是绑定在全局作用域中的。所以每一次循环,变量 i 的值都会发生改变,而循环内的console.log(i),里面的i指向的就是全局的i 。所以原代码等价于:
var i;
for (i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
再是因为js中函数的参数都是按值传递的
,所以每一次for循环时,都创建了一个 i 的副本,并把 i 的值传入setTimeout函数中的参数 i*1000 中去。所以结果才会是以每秒一次的频率输出数字。那为什么输出的是6,而不是我们期望的1到5呢?想知道答案就得了解setTimeout函数的运行机制。
setTimeout() 方法用于在指定的毫秒数后调用函数或计算表达式。setTimeout的延迟不是绝对精确的,它只是延迟一段时间后把该函数添加到执行队列当中,并不是在指定的毫秒数后立即执行。所以说如果指定的时间已经过去,但当前队列存在没有运行完的代码,该函数会等到排在队列中所有的函数运行完毕之后才会运行。也就是说所有传递给setTimeout的回调方法都会在整个环境下的所有代码运行完毕之后执行。
所以说,在执行setTimeout的回调时,for循环已经执行完毕,此刻的 i 的值为6,所以就算把上面代码的延迟时间改为0,也只会立即输出五个6。如:
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, 0 );
}
第一种方法:利用js中参数传递是按值传递的特征,可以简单的改造成下面的代码:
var output = function(i){
setTimeout( function timer() {
console.log( i );
}, i*1000 );
};
for (var i=1; i<=5; i++) {
output(i);
}
第二种方法:用立即执行函数表达式来创建作用域并传入参数
在迭代内使用IIFE 会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。如:
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout( function timer() {
console.log( j );
}, i*1000 );
})(i);
}
第三种方法:使用 ES6 let关键字
ES6 新增了let命令,用来声明变量。它所声明的变量,只在let命令所在的代码块内有效。而且let 声明的变量在循环过程中不止被声明一次,而是每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。
所以可以简单的写成下面这样:
for (let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
还有一点需要注意的是,在for循环中设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。如下面代码会立即输出五个0。
for (let i=1; i<=5; i++) {
let i = 0;
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}