JavaScript循环内的变量声明

8
/*Test scope problem*/
for(var i=1; i<3; i++){
    //declare variables
    var no = i;
    //verify no
    alert('setting '+no);

    //timeout to recheck 
    setTimeout(function(){
        alert('test '+no);
    }, 500);
}

这段代码在预期的情况下会触发"setting 1"和"setting 2"的警报,但是在超时后会输出两次"test 2" - 由于某种原因,在第一次循环后变量"no"没有被重置...

我只找到了一个"丑陋的"解决方法:

/*Test scope problem*/
var func=function(no){
    //verify no
    alert('setting '+no);

    //timeout to recheck 
    setTimeout(function(){
        alert('test '+no);
    }, 500);
}
for(var i=1; i<3; i++){
    func(i);
}

有没有更直接的方法来解决这个问题?还是这是唯一的方法?

1
我也很想看到关于 no 的作用域在这里发生了什么的详细解释。 - Daniel Bingham
2
@Daniel Bingham - no的作用域是全局的,除非Trouts是在函数内部执行。 (循环在js中不会设置新的作用域。) - user65663
@fig-gnuton 哦,那就是一个简单的解释了 :D 谢谢! - Daniel Bingham
4个回答

13

JavaScript没有块级作用域,变量声明会被提升。这两个事实意味着你的代码等价于:

var no;

/*Test scope problem*/
for(var i=1; i<3; i++){
    //declare variables
    no = i;
    //verify no
    alert('setting '+no);

    //timeout to recheck 
    setTimeout(function(){
        alert('test '+no);
    }, 500);
}

当超时函数执行时,循环已经完成,no仍然保持其最终值2。

解决方法是将no的当前值传递给一个函数,该函数为每个调用setTimeout创建一个新的回调函数。每次创建新的函数意味着每个setTimeout回调都绑定到不同的执行上下文中,具有自己的变量集。

var no;

/*Test scope problem*/
for(var i=1; i<3; i++){
    //declare variables
    no = i;
    //verify no
    alert('setting '+no);

    //timeout to recheck 
    setTimeout( (function(num) {
            return function() {
                alert('test '+num);
            };
        })(no), 500);
}

很好的答案,我认为它完全解释了这个问题。我最初认为在Javascript中范围总是在{}内工作,因此提出了这个问题。 - Carlos Ouro

2

这与您的修复方案基本相同,但使用不同的语法来实现作用域调整。

/*Test scope problem*/
for (var i = 1; i < 3; i++) {
  //declare variables 
  var no = i;
  //verify no 
  alert('setting ' + no);

  //timeout to recheck
  (function() {
    var n = no;
    setTimeout(function() { 
      alert('test ' + n);
    }, 500);
  })();
} 

1

我很喜欢能够从this answer中获得如此多的收益。

如果您需要帮助应用该答案,请告诉我。

编辑

好的。让我们看看您的原始代码。

//timeout to recheck 
setTimeout(function(){
    alert('test '+no);
}, 500);

看到那个匿名函数了吗?你传递给setTimeout()的那个?它只有在计时器到达500毫秒之后才会被调用,这已经是循环退出之后了。

你希望的是no在原地进行评估,但实际上它并没有 - 它是在函数被调用时评估的。此时,no的值为2。

为了解决这个问题,我们需要一个在循环迭代期间执行的函数,该函数本身将返回一个函数,setTimeout()可以按照我们期望的方式使用它。

setTimeout(function( value )
{
  // 'value' is closed by the function below
  return function()
  {
    alert('test ' + value );
  }
}( no ) // Here's the magic
, 500 );

由于我们创建了匿名函数并立即调用它,因此已创建了一个新的范围,我们可以在其中关闭变量。而该范围关闭在 value 周围,而不是 no。由于每次循环中 value 接收一个新的值,这些 lambdas 中的每一个都有自己的值 - 我们想要的那个值。

因此,当 setTimeout() 触发时,它执行的是从我们的闭包函数返回的函数。

希望这解释清楚了。


1
是的,我不明白这与问题有什么关系。您能向刚刚开始学习JavaScript的C / Java开发人员解释一下闭包吗? - Daniel Bingham

1
JavaScript没有词法作用域(for循环不会创建新的作用域),您的解决方案是标准的解决方法。另一种编写此代码的方式可能是:
[1, 2].forEach(function(no){
    //verify no
    alert('setting '+no);

    //timeout to recheck 
    setTimeout(function(){
        alert('test '+no);
    }, 500);
})

forEach() 是在 ECMAScript 5 中引入的,现代浏览器中都有支持,但 IE 不支持。不过你可以使用 我的库 来模拟它。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接