How to use the current value of a variable in a more internal function?

Asked

Viewed 5,488 times

38

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 on 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 to print variable value i corresponding to the iteration number? That is, the value of i at the time when the internal function was created in the setTimeout, resulting in texts from "Message 0" to "Message 49"

3 answers

31


Note:

In the modern version of Javascript (from ES6) this problem no longer happens if we use let in the for (let i = 0; ...

More about Let, cont and var here.


Answer to the question code:

The problem in the question code is that the cycle for is ready before the function setTimeout() run, thus the value of i It’s already 50 before the code textArea.value += 'Mensagem ' + i + '\n'; be run.

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

You can use it like 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


Other option, similar, but instead of having an external function, using a function that performs itself within the cycle for, and capturing 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, once the function setTimeout() accepts a third parameter into the function.

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

Example: setTimeout(Function(parameter){ /* code */ }, time, 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

  • 1

    Thank you, Segio. The first solution seems to be the cleanest, although I have to write more code. The second example was one I had thought about slightly different. The third seems to have problems with EI 8 and earlier.

  • 2

    Type answer "buy 1 light 3".

  • 1

    @utluiz, :) - Yes, I agree that the first is the best. In the first solution it may be better to use another variable name as a parameter of the function, in case there is some i closed in the same scope. I corrected the answer now too.

12

The name of the concept that’s causing you trouble is closure ("enclosure" in Portuguese, but this term is rarely used), and refers to how functions defined within a "lexical context" (i.e. the body of a function, a block, a source file) access variables defined in this context.

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 (inner) of an existing context has access to all variables defined in the "outside" (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 it does not matter when the internal function will perform, nor what value the external variables had at the time when the object function was created (in contrast to the function definition, which is at compile/interpretation time). What matters is that both share the same variable, and written on one side will reflect on the readings on the other and vice versa.

In your case, you are creating a new (anonymous) function within the lexical context of external code (another? script body function?), and it shares the variable i (and not the value of i). At the moment this function is executed, the loop for has changed its value several times, taking it to its maximum value (50) and that’s 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 i It would interfere with others too).

There are several ways to modify the code to achieve the desired behavior (i.e. a i different for each object-function) - as already pointed out by @Sergio - but my favorite is the one that makes the nature of closure more explicit (though it looks 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 anonymous function is not the same i passed as parameter to the same - since they are in different lexical contexts. It is also worth noting 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 also:

    (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 internal function still has access to the value it had at the time the loop was executed.

  • 1

    It would be good to highlight the return to return a function setting to setTimeout

  • @Sorry Victor, I don’t understand... You’re referring to the fact that what is passed to setTimeout is not the first function [the one that receives parameter], but the one that is returned by its invocation? That’s correct - although in this particular case it’s gotten a little strange. In fact, I’m going to rewrite this function to be more in line with the idea that I wanted to pass on.

  • 1

    Yes, that’s right! I just emphasized the fact that it is not something simple and clarify a little more for beginners this fact and explain better. Mainly for the fact that setTimeout receives a definition function.

6

With modern Javascript (ES-2015+)

The other answers remain 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 loop iteration will create a new variable, captured by closure passed to asynchronous function. The question code starts to work simply by replacing var for 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>

  • I find it interesting to add this link to the answer: https://answall.com/questions/47165/qual-%C3%A9-a-difference%C3%a7a-between-declare%C3%A7%C3%a3o-de-vari%C3%a1veis-using-Let-e-var, what do you think?

  • A year later... @Costamilam

  • Sooner or later than never :)

Browser other questions tagged

You are not signed in. Login or sign up in order to post.