Home  >  Q&A  >  body text

javascript闭包 - 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)
}

为可以这样写?

PHP中文网PHP中文网2749 days ago650

reply all(11)I'll reply

  • 天蓬老师

    天蓬老师2017-04-10 15:46:11

    //每次循环会调用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)
    }

    reply
    0
  • 阿神

    阿神2017-04-10 15:46:11

    因为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)
    }
    

    reply
    0
  • 高洛峰

    高洛峰2017-04-10 15:46:11

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

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

    reply
    0
  • PHP中文网

    PHP中文网2017-04-10 15:46:11

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

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

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

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

    reply
    0
  • 高洛峰

    高洛峰2017-04-10 15:46:11

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

    reply
    0
  • 高洛峰

    高洛峰2017-04-10 15:46:11

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

    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);

    reply
    0
  • 怪我咯

    怪我咯2017-04-10 15:46:11

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

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

    reply
    0
  • 巴扎黑

    巴扎黑2017-04-10 15:46:11

    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);
    }

    reply
    0
  • 伊谢尔伦

    伊谢尔伦2017-04-10 15:46:11

    可以一看:《JavaScript闭包详解》

    reply
    0
  • 黄舟

    黄舟2017-04-10 15:46:11

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

    reply
    0
  • Cancelreply