最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - setTimeout appears to be changing my variables! Why? - Stack Overflow

programmeradmin4浏览0评论

I'll be quick and jump straight to the case. The code is mented so you know my intentions. Basically, I'm building a small, HTML5 based game, and opposed to saving stuff on the server or in a cookie, I'll just provide the player a level code. When a player enters the code (in a form of a simple hash) into a text input field, and clicks a button to load that level, the function "l" is called. That function first retrieves the player's entries, then iterates through the list of hashes and pares them. When a match is fond, that certain level is supposed to be loaded, but there were errors. I did a little bit of debugging, and I found out that the value of the iterator ("i"), has changed inside a setTimeout! I want to pause 1 second, because loading the level immediately would be simply too quick and would look bad.

levelCodes = //Just a set of "hashes" that the player can enter to load a certain level. For now, only "code" matters.
[
    {"code": "#tc454", "l": 0},
    {"code": "#tc723", "l": 1},
]

var l = function() //This function is called when a button is pressed on the page
{
    var toLoad = document.getElementById("lc").value; //This can be "#tc723", for example

    for (i = 0; i < levelCodes.length; i++) //levelCodes.length == 2, so this should run 2 times, and in the last time i should be 1
        if (levelCodes[i].code == toLoad) //If I put "#tc723" this will be true when i == 1, and this happens
        {
            console.log(i); //This says 1
            setTimeout(function(){console.log(i)}, 1000); //This one says 2!
        }
}

I'll be quick and jump straight to the case. The code is mented so you know my intentions. Basically, I'm building a small, HTML5 based game, and opposed to saving stuff on the server or in a cookie, I'll just provide the player a level code. When a player enters the code (in a form of a simple hash) into a text input field, and clicks a button to load that level, the function "l" is called. That function first retrieves the player's entries, then iterates through the list of hashes and pares them. When a match is fond, that certain level is supposed to be loaded, but there were errors. I did a little bit of debugging, and I found out that the value of the iterator ("i"), has changed inside a setTimeout! I want to pause 1 second, because loading the level immediately would be simply too quick and would look bad.

levelCodes = //Just a set of "hashes" that the player can enter to load a certain level. For now, only "code" matters.
[
    {"code": "#tc454", "l": 0},
    {"code": "#tc723", "l": 1},
]

var l = function() //This function is called when a button is pressed on the page
{
    var toLoad = document.getElementById("lc").value; //This can be "#tc723", for example

    for (i = 0; i < levelCodes.length; i++) //levelCodes.length == 2, so this should run 2 times, and in the last time i should be 1
        if (levelCodes[i].code == toLoad) //If I put "#tc723" this will be true when i == 1, and this happens
        {
            console.log(i); //This says 1
            setTimeout(function(){console.log(i)}, 1000); //This one says 2!
        }
}
Share Improve this question asked Mar 2, 2012 at 20:56 corazzacorazza 32.4k39 gold badges120 silver badges191 bronze badges 5
  • 2 Nice menting, have all the upvotes. – ninjagecko Commented Mar 2, 2012 at 21:13
  • FYI, your code does not pass jslint for several reasons and will produce errors in some browsers – Mark Schultheiss Commented Mar 2, 2012 at 21:32
  • @MarkSchultheiss, please explain, I think that jsLint is some kind of an evaluator, is that correct? Well then, can you tell me why doesn't this pute? – corazza Commented Mar 2, 2012 at 21:47
  • 1 Probably the easiest way is to look at: jsfiddle/h9tNJ - click the jslint button. - note the extra ma will cause errors in IE for sure. – Mark Schultheiss Commented Mar 2, 2012 at 21:51
  • Oh, I see. That is also a very useful tool! I usually do put all that stuff in, and I do watch out, I guess this was just an exception... – corazza Commented Mar 2, 2012 at 21:56
Add a ment  | 

5 Answers 5

Reset to default 7

The others already wrote the reason for the behaviour you're getting. Now the solution: change the setTimeout line to:

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

This works because it captures the current value of the variable i into yet another closure, and the variable within that closure doesn't change.

ECMAscript uses the technique of lexical closures which is nothing else than an internal storage to all parent context objects / lexical environment records (ES3 / ES5).

In short words, your anonymous function used by setTimeout closes over that i variable, so while that timeout is "waiting", your loop continues. setTimeout operatores asynronously of course and that in turn means, at the time that loop has finished the value if i is of course 2.

Now remember about the closure stuff, our anonymous function in setTimeout still holds a reference to i when it finally fires (after 1000ms). So it correctly shows you the value of 2.

If you want to show the numbers for each iteration after 1000ms, you need to invoke another context. This might look similar to

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

The for loop continuously increments i until the loop condition is met, even though the code in the for loop does not execute, when the code in the setTimeout executes it shows the current value of i - which is 2.

By the time setTimeout callback gets to execute variable i will have a value of 2, because you don't quit the loop and i keeps counting until it's equal to levelCodes.legnth (which is 2). Basically you need to add return or break after calling setTimeout. Also, you are not declaring variable i, so it will be bound to global namespace, which is bad and may potentially lead to very obscure bugs. For example, the value of i can be changed in other functions so setTimeout callback will see a different value. You need to add var i; in the beginning of function l.

You can pass the third parameter which is about to change after the timeout duration and that will be your first argument of the function. eg.

setTimeinterval(function(x){console.log(x)},1000,i)

发布评论

评论列表(0)

  1. 暂无评论