How to use the current value of a variable in an inner function?

34

The following code "prints" 50 times the text "Message 50" in a textarea .

for (var i = 0; i < 50; i++) {
    setTimeout(function() {
        textArea.value += 'Mensagem ' + i + '\n';
        textArea.scrollTop = textArea.scrollHeight;
    }, 100 * i);
}

See the example in jsfiddle .

I understand that this occurs because the value of i is updated in the external function, and at the time of execution of the internal function, the reference points to the updated value.

How do I print the value of the variable i corresponding to the iteration number? That is, the value of i at the time the internal function was created in the setTimeout call, resulting in texts from "Message 0" to "Message 49"

    
asked by utluiz 27.12.2013 в 14:47
source

3 answers

29

Note:

In the modern version of JavaScript (as of ES6) this problem will no longer happen if we use let in for (let i = 0; ...

More about let, cont and var here .

Answer to question code:

The problem in the question code is that the for loop is ready before the setTimeout() function runs, so the i value is already 50 before the textArea.value += 'Mensagem ' + i + '\n'; code is run.

Calling setTimeout() via function extenes to the loop cycle, then the value of i that is passed as parameter is the value of each iteration.

You can use this:

var textArea = document.getElementById('a');
var tempo = function (pTempo) {
    setTimeout(function () {
        textArea.value += 'Mensagem ' + pTempo+ '\n';
        textArea.scrollTop = textArea.scrollHeight;
    }, 100 * pTempo);

}
for (var i = 0; i < 50; i++) {
    tempo(i);
}

Example

Another option , similar, but instead of having an external function, using a function that auto-executes within the for loop, and which captures the value of i :

var textArea = document.getElementById('a');
for (var i = 0; i < 50; i++) {
    (function () {
        var iScoped = i;
        setTimeout(function () {
            textArea.value += 'Mensagem ' + iScoped + '\n';
            textArea.scrollTop = textArea.scrollHeight;
        }, 100 * iScoped );
    })()
} 

Example

There is yet another way, since the setTimeout() function accepts a third parameter into the function.

  

var timeoutID = window.setTimeout (func, delay, [param1, param2, ...]);

     

Example:    parameter ); parameter

Source: MDN English

var textArea = document.getElementById('a');
for (var i = 0; i < 50; i++) {
    setTimeout(function(i) {
        textArea.value += 'Mensagem ' + i + '\n';
        textArea.scrollTop = textArea.scrollHeight;
    }, 100 * i, i);
}

Example

    
answered by 27.12.2013 / 14:54
source
10

The name of the concept that is causing you confusion is closure (

In JavaScript, only functions define a new lexical context (other languages have different rules - some even support the concept of closure):

var a = 10; // Mesmo "a" para script1.js, script2.js, etc (efetivamente, uma global)
function f() {
    var b = 20; // Um "b" diferente para cada invocação de f
    if ( x ) {
        var c = 30; // Mesmo "c" dentro e fora do if (i.e. o contexto é "f", não o bloco if)

And each new context created within an existing context has access to all the variables defined in the "outer" :

>
function x(a1) {          // "x" tem acesso a "a"
    var a2;
    function y(b1) {      // "y" tem acesso a "a" e "b"
        var b2;
        function z(c1) {  // "z" tem acesso a "a", "b", e "c"
            var c2;

It is important to note that when the internal function will run, nor what value the external variables had at the time the object function was created (by contrast with the definition of the function, which is at compile / interpretation time). What matters is that both share the same variable, and written on one side will reflect on each other's readings and vice versa.

In your case, you are creating a new (anonymous) function within the lexical context of the external code (another function? body of the script?), and it shares the variable i not value of i ). At the moment this function is executed, the for loop has already changed its value several times, taking it to its maximum value ( 50 ) and this is what the internal function will access. If the external code modified or reused i for other purposes, this would also be reflected in the internal function (and likewise, if one of the function objects changed that i it would interfere with the others as well.)

There are several ways to modify the code to achieve the desired behavior (ie a i different for each object-function) - as already pointed out by @Sergio - but my favorite is that which makes the nature of < in> closure more explicit (even if it seems visually "strange" to anyone unfamiliar with the concept):

for (var i = 0; i < 50; i++) {
    (function(i) {
        setTimeout(function() {
            textArea.value += 'Mensagem ' + i + '\n';
            textArea.scrollTop = textArea.scrollHeight;
        }, 100 * i);
    })(i);
}

Note that the i argument of the anonymous function is not the same as i passed as parameter to it - since they are in different lexical contexts. It should also be noted that the variable textArea is still coming from the external context, and depending on the case it may be interesting to include it in the closure as well:

    (function(i, textArea) { ... })(i, textArea);

This ensures that - even if this variable has its value changed (i.e., it points to a different element) - the inner function still has access to the value it had at the time the loop was executed.     

answered by 27.12.2013 в 18:15
4

With modern JavaScript (ES-2015 +)

The other answers are still valid, but nowadays we have a standard solution for this with modern JavaScript, using variables with block scope declared with let . You should use this solution if:

With block-scoped variables, each iteration of the loop will create a new variable, captured by the closure passed to the asynchronous function. The question code starts to work simply by replacing var with let :

const textArea = document.querySelector('textarea');

for (let i = 0; i < 50; i++) {
    setTimeout(function() {
        textArea.value += 'Mensagem ' + i + '\n';
        textArea.scrollTop = textArea.scrollHeight;
    }, 100 * i);
}
<textarea></textarea>
    
answered by 13.10.2017 в 16:49