Search code examples
javascriptmemory-managementclosuresheap-memorylocal-variables

JavaScript Closures and Block-Scoped Variables In Loop Memory Management


This question is a few different (I think related) questions, which I give below, but broadly I am trying to understand a code piece from David Flanagan's O'Reilly book on JavaScript that gives an example as using a loop for creating closures as unintended:

function constfuncs() {
    let funcs = [];
    for(var i = 0; i < 10; i++) {
        funcs[i] = () => i;
    }
    return funcs;
}

let funcs = constfuncs();
funcs[5]() //10, rather than 5

The above code example intends to have each element of the funcs array be a function that can be called without arguments to return a value numerically equivalent to its index in funcs; however, it does not succeed at doing this. I think I largely understand why this is: var i, because it is declared using var, is scoped to the function constfuncs, rather than the block of that for loop, and so hence it is the same i being used by each function stored in funcs, and i ends as 10 in that loop, so hence any of the ten returned functions in funcs will always evaluate to 10 when called.

On the next page of the book, it's stated that to fix the above, it's a one-line change: simply have i be block-scoped, and so use let i instead of var i in that for loop, as follows:

function constfuncs() {
    let funcs = [];
    for(let i = 0; i < 10; i++) {
        funcs[i] = () => i;
    }
    return funcs;
}

let funcs = constfuncs();
funcs[5]() //is now 5, as desired

What I don't understand, however, is how this fixes the problem. i to me seems to still exist only once, yet each of the ten created functions appears to have access to its own version/copy of i in memory. If a new block-scoped local variable was declared in the for loop that was assigned the value of i on each iteration prior to then being used instead of i as the value each function of funcs returned, then perhaps it would make more sense to me, if it is the case that block-scoped local variables declared in a loop are redeclared (as using new memory) on each iteration; but even so, i is not and yet the above still works as desired. This leads me to my questions:

  1. How are closures managed in memory in JavaScript? I am fairly new to JS and closures, and am used to languages having a stack and heap.
  2. Does the act of defining a closure cause a copying of memory from the stack to the heap to save outer local variables for use by that closure? This is because a closure seems to defy local variables only being stored on the stack, as it can still access them.
  3. Are block-scoped local variables re-created in memory (at a different memory location each time) for each iteration of an outer loop they are declared in?
  4. If possible yes to 3., is this influenced at all by whether a closure is used (ie, only created once in memory across loop iterations as the general case, but on each loop iteration if a closure has that local block variable in scope for it)?
  5. By "binding", do we mean a distinct location for a variable or constant in memory? I ask as the book states in its explanation for the above solution that "Because let and const are block scoped, each iteration of the loop defines a scope that is independent of the scopes for all other iterations, and each of these scopes has its own independent binding of i."

Questions 1-2 are related, as are 3-4; and all are related to the above example. Any other thoughts for explaining why the second approach above (with let i) succeeds in fixing the solution are welcome.


Solution

  • Technically I'm not answering any of you questions, but I can explain the part where you wondered "how this fixes the problem":

    Variables declared with let in the for-loop initialization are treated as if they were declared inside the loops "body", by definition. That is a special behavior of for-loops with let.

    Variables declared with var on the other hand have the same scope as where the for-loop is defined (i.e. outside of the loops "body").

    Examples

    So if you would declare the let i outside the for-loop, you would get the wrong value 10 again (as you expected):

    function constfuncs(){
        let funcs = [];
        let i;
        for( i = 0; i < 10; i++ ){
            funcs[ i ] = function(){
                return i;
            };
        }
        return funcs;
    }
    

    On the other hand, even if you keep the var inside the loop initialization, but declare a new variable with let inside the loops body, you would get the desired value 5:

    function constfuncs(){
        let funcs = [];
        for( var i = 0; i < 10; i++ ){
            let value = i;            // <-- new variable in every iteration
            funcs[ i ] = () => value;
        }
        return funcs;
    }
    

    Remarks

    The newer let declaration is specifically designed for this behavior, because people tended to be confused about the behavior of var.

    (I case you wonder: IMO let has too many special cases. let makes the behavior more intuitive when you don't think about it, but more complicated if you want to understand it. But it still solves important flaws of var.)