JavaScript循环内的闭包为什么返回的是最后一个值


错误写法


 for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);  
    }, 1000);
}

为什么这样写是错的

正确写法


 for(var i = 0; i < 10; i++) {
    (function(e) {
        setTimeout(function() {
            console.log(e);  
        }, 1000);
    })(i);
}

为可以这样写?


 for(var i = 0; i < 10; i++) {
    setTimeout((function(e) {
        return function() {
            console.log(e);
        }
    })(i), 1000)
}

为可以这样写?

javascript对象 javascript闭包 JavaScript

糖果店老板 10 years ago
射得满满的 answered 10 years ago

One often made mistake is to use closures inside of loops, as if they were copying the value of the loop's index variable.


 for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);  
    }, 1000);
}

The above will not output the numbers 0 through 9, but will simply print the number 10 ten times.

The anonymous function keeps a reference to i. At the time console.log gets called, the for loop has already finished, and the value of i has been set to 10.

In order to get the desired behavior, it is necessary to create a copy of the value of i.

Avoiding the Reference Problem


 for(var i = 0; i < 10; i++) {
    (function(e) {
        setTimeout(function() {
            console.log(e);  
        }, 1000);
    })(i);
}

The anonymous outer function gets called immediately with i as its first argument and will receive a copy of the value of i as its parameter e.

The anonymous function that gets passed to setTimeout now has a reference to e, whose value does not get changed by the loop.

There is another possible way of achieving this, which is to return a function from the anonymous wrapper that will then have the same behavior as the code above.


 for(var i = 0; i < 10; i++) {
    setTimeout((function(e) {
        return function() {
            console.log(e);
        }
    })(i), 1000)
}

The other popular way to achieve this is to add an additional argument to the setTimeout function, which passes these arguments to the callback.


 for(var i = 0; i < 10; i++) {
    setTimeout(function(e) {
        console.log(e);  
    }, 1000, i);
}

Some legacy JS environments (Internet Explorer 9 & below) do not support this.

There's yet another way to accomplish this by using .bind, which can bind a this context and arguments to function. It behaves identically to the code above


 for(var i = 0; i < 10; i++) {
    setTimeout(console.log.bind(console, i), 1000);
}

kaien answered 10 years ago

原来如此!!!

鳯条院聖华 answered 10 years ago

解决这个问题的关键:弄清楚每种写法的作用域链


 for(var i = 0; i < 10; i++) { // 作用域A,存储i的值

setTimeout(function() {//作用域B
    console.log(i);  
}, 1000);
}

闭包的形成使得外部代码块执行完毕,其变量仍然驻留在内存中。
代码块B在执行时,找不到变量i,于是沿着作用域链向上找,取到A作用域中i的值,此时内存中i值为10


 for(var i = 0; i < 10; i++) { //作用域A,存储i

(function(e) {   //作用域B0,存储e0 作用域B1,存储e1,每循环一次,都有一个单独的作用域
    setTimeout(function() {//作用域C0,C1,C2,... 对应外部作用域B0,B1...
        console.log(e);  
      }, 1000);
    })(i);
}

理解了原理,另一种写法也是类似的,通过延长作用域链来保存每个i的值
还有一点就是, (function(e){})(i) (匿名函数)可以理解为


 var sum = function(e){};
sum(i);

天王寺胡太郎 answered 10 years ago

从作用域角度来解答,for循环中i是全局的,所以你的第一种写法i没有作用域限制会不对,第二种闭包写法把i当参数传入,会在i在局部作用完,走下一个

我是偶像派 answered 10 years ago


 setTimeout((function(e) {
    return function() {
        console.log(e);
    }
})(i), 1000)

如上代码, setTimeout 的第一个参数是一个即时执行函数表达式,也是一个闭包:


 (function(e) {
    return function() {
        console.log(e);
    }
})(i)

该函数把循环中的 i 的值作为传参 e 传入该闭包,该闭包返回另一个函数,该函数的作用域中的 e 是循环时候传入的值,而不是循环结束后的 i

Onice answered 10 years ago

你写成这样或许能更好明白点。


 for(var i = 0; i < 10; i++) {
    setTimeout((function(e) {
        console.log(e);
        return function() {
            console.log(e);
        }
    })(i), 1000)
}

werwolf answered 10 years ago

因为ES6之前没有块作用域


 for(var i = 0; i < 10; ++i){
  setTimeout(function(){
    console.log(i)
  }, 1000)
}

有块作用域时效果如同


 for(var i = 0; i < 10; ++i){
  var j = 0
  j = i
  setTimeout(function(){
    console.log(j)
  }, 1000)
}

就是块内代码引用的i变量均不是指向同一个变量。
而ES6则引入let关键字来标识变量位于块作用域内


 for(let i = 0; i < 10; ++i){
  setTimeout(function(){console.log(i)}, 1000)
}

当然在ES3/5下除了通过IIFE构造作用域外,还可以通过with来构造


 for(var i = 0; i < 10; ++i) with({i:i}){
   setTimeout(function(){console.log(i)}, 1000)
}

抹小布poi answered 10 years ago

其实原理就是让函数把变量复制一个副本,保存起来。

如果不以参数形式传入,使得以闭包形式保存的话,则会向上级作用域引用变量,也就是你说的i会是最后一个

一刀砍死兄贵 answered 10 years ago


 //每次循环会调用setTimeout函数,其中指定了一个timeout后执行的函数
//这个函数因为构成闭包的关系,其能够访问外层函数定义的变量,这个变量就是i
//在for循环执行完毕后,i的值为10.此时在事件队列中有10个timeout函数等待执行
//当timeout时间到时,对应的执行函数调用的i都是同一个,也就是10
for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);  
    }, 1000);
}

//在for循环中定义了匿名立即执行函数
//通过将每次循环时产生i传入匿名立即执行函数,立即执行函数就有了一个内部变量e,
//其值是传入的i
//setTimeout函数形成闭包,能访问到其外层函数也就是匿名立即执行函数的变量e
//因为e引用关系的存在,匿名立即执行函数不会被马上销毁掉
//timeout时间一到,指定执行函数调用的e就是每次传入的参数i
for(var i = 0; i < 10; i++) {
    (function(e) {
        setTimeout(function() {
            console.log(e);  
        }, 1000);
    })(i);
}

//整个和上面的类似,只不过把匿名立即执行函数传递给setTimeout的第1个参数中
//匿名立即执行函数,顾名思义就是需要立即执行的呀。
//所以setTimout函数对应的超时执行函数(第1个参数)
//为匿名立即执行函数执行的结果,也就是返回的函数。
//接下来理解就和上面一样啦
for(var i = 0; i < 10; i++) {
    setTimeout((function(e) {
        return function() {
            console.log(e);
        }
    })(i), 1000)
}

OHshit answered 10 years ago

Your Answer