JavaScript增量不起作用。

5

我不知道该给这篇文章起什么标题会更好,因为我面临的情况非常特殊,或者说是我太蠢了。

我正在尝试创建一个简单的HTML5新标签<meter>。主要问题出在我的JavaScript上。我试图逐渐增加meter标签的值,但是它似乎没有按照我的意愿运作。

以下是JavaScript代码:

for (var i = 0; i <= 10; i++) {
    var a = document.getElementById("mtr1");
    setTimeout(function () {
        console.log(i);
        a.value = i;
    }, 250);
}

我试图每250毫秒逐渐增加计数器的值,但这并没有发生。相反,计数器直接跳到了10。
我在控制台中得到的i的值很有趣。我得到了实例10,而不是1、2、3……10。
为什么会这样呢? FIDDLE

1
setTimeout是一个延迟事件,它不会在内联运行,因此您的for循环将在函数未运行的情况下退出,然后10秒钟后它将增加a。 - Liam
1
这个问题已经在这里被回答了很多次,搜索JavaScript闭包和作用域即可。 - Matteo Tassinari
可能是JavaScript闭包和setTimeout的重复问题。 - Liam
那么多错误的答案…… - Liam
是的,也许这是一个重复的问题。但并不是非常常见的问题。根据我得到的许多错误答案,我仍然认为许多人不知道这个问题。 - MarsOne
我希望人们不要因为这个问题已经有了许多好的答案而关闭它。我已经收到了四种不同的解决方案,所以请将其保持在活跃状态。 - MarsOne
9个回答

7

这是一个关于JavaScript闭包的经典例子。在这里,i实际上是变量的引用,而不是它的副本。当您通过循环迭代后,它的值为10,这就是为什么所有的日志调用都会写入10到日志中。
这样做会更好:

for (var i = 0; i <= 10; i++) {
    var a = document.getElementById("mtr1");
    setTimeout(function (i) {
        return function() {
            console.log(i);
            a.value = i;
        };
    }(i), 250 * i);
}

在这里,最内层的isetTimeout回调参数,而不是您在循环体中声明的变量。


我刚刚在写这个。 - j0hnstew
嗯,它将i的所有值从1到10写入日志,并将i的值设置为仪表。你能告诉我哪里出了问题吗? - aga
是的。但延迟并不会在每次摩擦后发生。请查看此链接以了解我的预期 http://jsfiddle.net/Qh6gb/11/ - MarsOne
我必须将不同的延迟设置到所有回调函数中,而不是所有回调函数都设置为250。 - aga
1
很高兴能帮助你。 :) @MarsOne,我个人推荐你阅读Douglas CrockFord的《JavaScript: The Good Parts》。它很小(大约150页左右),绝对值得一读,读完后我可以理解plalx解决方案的美妙之处。 - aga
显示剩余4条评论

7

您应该了解JavaScript中的闭包。当一个变量被封闭时,它是同一个变量,而不是一个副本。因为setTimeout是异步的,所以整个循环在任何函数运行之前都已经完成,因此i变量将在所有地方都是10

演示

function incMtrAsync(max, delay, el) {
    if (el.value++ < max) {
        setTimeout(incMtrAsync.bind(null, max, delay, el), delay);
    }   
}

incMtrAsync(10, 250, document.getElementById("mtr1"));

上述实现使用递归方法实现循环。每次调用inMtrAsync时,它都会检查计量表的value是否达到最大值,如果没有,则注册另一个超时回调自身的函数。
如果您想要延迟初始增量,只需在第一次调用中再包装一个超时函数即可。
setTimeout(incMtrAsync.bind(null, 10, 250, document.getElementById("mtr1")), 250);

我不知道闭包与setTimeout有什么共同点,但其余部分是最好的解决方案。+1 - Entity Black
+1 对于可行的解决方案。但对我来说有点复杂,所以我会选择一个更简单的方案。 - MarsOne
@MarsOne 是的,链接现在已经修复了。我强烈建议您使用这种方法而不是其他方法。 - plalx
你给我的函数名看起来很吓人。不管怎样,你是对的,我会使用它。但我真的很抱歉。我不想改变我的解决方案标记。希望你能理解。 - MarsOne
@MarsOne 很重要的是你理解JS中这些概念。有问题就问我吧 ;) 函数名incMtrAsync可以随意更改,这里代表incrementMeterAsynchronously - plalx
1
@plalx 的解决方案非常优美,给你点赞。 - aga

5
没人使用 setInterval,这里是我的解决方案(http://jsfiddle.net/Qh6gb/4/):
var a = document.getElementById("mtr1");
var i = 0;
var interval = setInterval(function () {
    console.log(i);
    a.value = ++i;
    if (i == 10) {
        clearInterval(interval);
    }
}, 250);

我喜欢这种方法。看起来足够简单。 - MarsOne
是的,这是另一种干净的做法。 - plalx

3

您所描述的问题出现在异步调用 setTimeout 之前,因为在回调执行时 i 的值为 10 ,这是它的值。

因此,这是闭包范围的问题,为了使其正常工作,您应该像这样做:

for (var i = 0; i <= 10; i++) {
    var a = document.getElementById("mtr1");
    (function (i, a) {
        setTimeout(function () {
          console.log(i);
          a.value = i;
        }, 250);
    })(i, a);
}

此外,由于a始终相同,因此以下代码应该更好:
var a = document.getElementById("mtr1");
for (var i = 0; i <= 10; i++) {
    (function (i) {
        setTimeout(function () {
          console.log(i);
          a.value = i;
        }, 250);
    })(i);
}

如果你希望看到计数器“滴答”上升,可以将它显示出来:

var a = document.getElementById("mtr1");
for (var i = 0; i <= 10; i++) {
    (function (i) {
        setTimeout(function () {
          console.log(i);
          a.value = i;
        }, 1000 * i);
    })(i);
}

请参见 http://jsfiddle.net/LDt4d/

2
这是因为您调用了setTimeout,它是“异步”的。因此,setTimeout被调用了10次,但在整个循环完成后才执行。因此,在每次调用中i都等于10...

http://jsfiddle.net/Qh6gb/9/

这是解决方案:
var i = 1,
    meter = document.getElementById("mtr1");

function increase() {
    meter.value = i++;
    console.log(i);
    if(i<=10) {
        setTimeout(increase, 250);
    }
}

setTimeout(increase, 250);

是的,我也不太明白... 我的第一次解释是正确的,只是有点简短了... - Entity Black
1
我能想到的唯一一件事就是使用“异步”这个词,尽管JavaScript看起来像是以这种方式运行,但它从未是异步的。它使用延迟模型而不是多线程。 - Liam

1

您可以使用 timeout jQuery 插件:它更加易用。

然而,您需要计算超时时间,

对于您来说,timeout=250*max=250*10=2500

因此,

$('meter').timeout(2500);

演示


0

在函数内部运行for循环,而不是在每个循环步骤中声明闭包。

JSFIDDLE: http://jsfiddle.net/Qh6gb/3/

var a = document.getElementById("mtr1");
setTimeout(function () {      
    for (var i = 0; i < 10; i++) {
        console.log(i);
        a.value = i;
    }
}, 250);

1
这就是你想要的。 - j0hnstew
1
他希望每次迭代之间相隔250毫秒执行。 - Matthew Mcveigh

0

我希望我理解得对。请尝试并告诉我是否有解决方案。

var count = 0;
function increment(){
    document.getElementById("meter").value = count; 
    count++; 
    if(count ==10) 
        count=0;
}
setInterval(increment, 250);

请与jsFiddle确认一下。


-1

您正在创建多个同时触发的函数。 将计时器乘以i以获得正确的延迟。

for (var i = 0; i <= 10; i++) {
var a = document.getElementById("mtr1");
setTimeout(function () {
    console.log(i);
    a.value = i;
}, 250 * i);
}

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