Free email newsletter: “ES.next News

2011-02-17

JavaScript variable scoping and its pitfalls

This blog post explains how variables are scoped in JavaScript. It points out two things that can cause problems if you are not aware of them: JavaScript variables are function-scoped and they can be captured in closures.

The scope of a variable defines where the variable is accessible. For example, if a variable is declared at the beginning of a function, it is accessible from within that function, but not from outside and usually dies when the function is finished. In this case, the function is the scope of the variable. When a scope is entered, a new environment is created that maps variable names to values. Scopes can be nested. A variable is accessible in its scope and in all scopes nested within that scope.

Pitfall 1: Variables are function-scoped. Most mainstream languages are block-scoped – new environments are created when entering a block and scopes are nested by nesting blocks. In contrast, JavaScript variables are function-scoped – new environments are only created when entering a function and scopes are nested by nesting functions. That means that even if you declare a variable inside a block such as the “then” block of an if statement, it is accessible everywhere in the surrounding function. The following code illustrates this.

    var myvar = "global";
    function f() {
        print(myvar); // (*)
        if (true) {
            var myvar = "local"; // (**)
        }
        print(myvar);
    }
    > f();
    undefined
    local
    > myvar
    global
As you can see, even the first access of myvar refers to the local value of it (which is not yet assigned at (*)). The reason for this is that var-declared variables are hoisted in JavaScript: var myvar = "local" is equivalent to declaring myvar at the beginning of f() and performing a simple assignment at location (**). Thus, it is a best practice in JavaScript to only use var at the beginning of a function.

Pitfall 2: Closures. Scoping in JavaScript is static, it is determined by the nesting of syntactic constructs. To ensure static scoping, the environment is attached to values that access variables in the environment. An example of such a value is the returned function in the following code.

    function f() {
        var x = "abc";
        return function() {
            return x;
        }
    }
Interaction:
    
    > var g = f();
    > g()
    abc
Variable x is free within the returned function, it cannot be resolved within it. By attaching the environment, x can be resolved to the value we expect given static scoping. This pairing of a value with an environment is called a closure, because the free variables are closed over. JavaScript’s closures are very powerful. You can even use them to store the properties of an object, as demonstrated in the following code.
    function objMaker(color) {
        return {
            getColor: function() {
                return color;
            },
            setColor: function(c) {
                color = c;
            }
        };
    }
Interaction:
    > var c = objMaker("blue");
    > c.getColor()
    blue
    > c.setColor("green");
    > c.getColor()        
    green
The value of color is stored in the environment that has been created when invoking objMaker(). That environment has been attached to the returned value, which is why color is still accessible even after objMaker() has terminated.

Inadvertently sharing an environment. Closures plus function-scoped variables lead to unexpected behavior. Given the following code.

    function f() {
        var arr = [ "red", "green", "blue" ];
        var result = [];
        for(var i=0; i<arr.length-1; i++) {
            var func = function() {
                return arr[i];
            };
            result.push(func);
        }
        return result;
    }
This function returns an array with two functions in it. Both of these functions can still access f’s environment and thus arr. In fact, they share the same environment. But in that environment, i has the value 2 and thus both functions return "blue" when invoked:
    > f()[0]()
    blue
This is not what we wanted. In order for this to work, we need to make a snapshot of the index i before creating a function that uses it.

Creating new environments. In block-scoped programming languages the following would work.

    function f() {
        var arr = [ "red", "green", "blue" ];
        var result = [];
        for(var i=0; i<arr.length-1; i++) {
            // New environment for every iteration? Not in JS!
            var color = arr[i]; // NOT a fresh copy in JS!
            var func = function() {
                return color;
            };
            result.push(func);
        }
        return result;
    }
JavaScript is function-scoped, so color is handled as if it had been declared at the beginning of f() and we don’t get a different environment for each func that we return. Only functions can create environments, so we use a function to simulate an environment-creating block. This looks as follows:
    (function() { // open block
        // inside block
    }()); // close block
Because we immediately invoke the function, the behavior regarding the wrapped code is the same as for blocks: The code is executed right away. Why the parentheses around this construct? They are there to avoid a syntax error. If a statement starts with function, a function declaration is expected. Thus, without the opening parenthesis, the parser complains about a missing name for the declaration. You might get the idea to provide such a name, but then the parser complains about the empty parentheses following the function declaration. In either case, the function would not be executed. Thus, we use the opening parenthesis to indicate that an expression will follow, a function expression. This is called an Immediately Invoked Function Expression (IIFE, pronounced “iffy”). Rewriting the pseudo-code above with an IIFE leads to the following code.
    function f() {
        var arr = [ "red", "green", "blue" ];
        var result = [];
        for(var i=0; i<arr.length-1; i++) {
            (function() {
                var color = arr[i]; // fresh copy
                var func = function() {
                    return color;
                };
                result.push(func);
            }());
        }
        return result;
    }
Now a new environment is created for each iteration of the loop. The result is as expected:
    > f()[0]()
    red
You can also make the color a parameter of the IIFE.
    function f() {
        var arr = [ "red", "green", "blue" ];
        var result = [];
        for(var i=0; i<arr.length-1; i++) {
            (function(color) {
                var func = function() {
                    return color;
                };
                result.push(func);
            }(arr[i]));
        }
        return result;
    }
Note that the example does have real-world relevance, because similar scenarios arise when adding handlers to DOM elements.

Flattr

No comments: