Understanding JavaScript’s scopes

Sometimes, I see odd JavaScript-code, like this:

1
2
3
4
5
6
7
8
9
// declares var "i":
for (var i=0; i<2; i++) {
    // ...
}
 
// redeclares var "i":
for (var i=0; i<2; i++) {
    alert("I don't understand JavaScript's scopes");
}

The author of this code (obviously) didn’t really understand JavaScript’s scoping rules. Often, that code’s origin is a developer, whose first/main programming language is one, where block parenthesis causes new scopes to be created. But for JavaScript this isn’t true.

Preface

In the following, we’re going to talk about JavaScript based on ECMA-Script 262 in strict mode.

What is a scope?

A scope “encapsulates” and modifies the way the code’s “identifier resolution” will behave. Thus, a “scope” determines “which x is meant here“. In the following example, the comments reflect the virtual machine’s “resolval behaviour” when asking for some “x”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var x = 41;
 
// x is defined in the global scope here
 
function incX() {
    // x is no parameter, and no local variable, so, it's the global one:
    x++;
 
    // the global x is now 42
 
    function y(x) {
        // x now is a parameter, which "hides" the global x, and prevents
        // that modifications of "x" are modifications of the GLOBAL "x"
 
        x = "bam!";
    }
 
    // x is the global x again here, STILL being 42
}

When do we enter a new scope?

The only way to create a new scope is the introduction of a function (or a Function-instance), “wrapping” the corresponding code in a new execution context:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// "outer scope" i:
var i=17;
 
// will log that i is 17
console.log("before closure i is: " + i);
 
// the following is a "closure", an anonymous function without name,
// which we declare and immediately invoke:
(function() {
    // in here, we're in another scope, so modifying "i"
    // won't change the outer scope's "i" at all
 
    // "inner scope" i:
    var i=false;
 
    // will log that i is now resolved to be false
    console.log("inside closure i is: " + i);
})();
 
// will log that i is 17 again, as we left the inner scope
// and are in the outer scope again
console.log("after  closure i is: " + i);

Don’t be confused about that (function() {...})(); construct. The execution order of the code above will (still) be “from the first to the last line”. But the anonymous function, the in here-Block will be executed in an own scope, so changes to i of the inner scope won’t cause the i of the outer scope to be touched.

But why? For a better understanding, let’s have a look at the ECMA-262 standard chapter 10.4, “Establishing an Execution Context”

Every invocation of an ECMAScript code function (13.2.1) also establishes and enters a new execution context, even if a function is calling itself recursively.

Doesn’t sound too complicated – if the executed code enters a function by invocation, it creates a new Execution Context. What’s that, an “Execution Context“? Well, let’s keep things short, a short excursion into ECMA-262 standard chapter 10.3, “Execution Contexts”, states that an Execution Context holds the following state components:

  1. LexicalEnvironment: Identifies the Lexical Environment used to resolve identifier references made by code within this execution context
  2. VariableEnvironment: Identifies the Lexical Environment whose environment record holds bindings created by VariableStatements and FunctionDeclarations within this execution context
  3. ThisBinding: The value associated with the this keyword within ECMAScript code associated with this execution context

The LexicalEnvironment- and VariableEnvironment-components are relevant “how” variables and identifiers will be “resolved”, means “which x is meant in the following example”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// global scope declaration:
var x="outer";
 
(function() {
    // as we immediately execute this closure, we're
    // in a new execution context now, thus: a new scope
    var x="in-closure";
 
    function bam() {
        // and one more "new" execution context as soon
        // this function will be invoked
 
        var x="inside-function-inside-closure";
    }
 
    bam();
 
    // here, x is "in-closure" again
})();
 
// here, x is "outer" again

No block scopes in JavaScript!

Just to make it really clear: the invocation of a function is the only way to create a new execution context, and thus, a new child scope! In opposite to e.g. C++, JavaScript does not have block scopes, so this does not work in JavaScript:

1
2
3
4
5
6
7
8
9
    var name = "Adam";
 
    if (true) {
        // block parenthesis: STILL the same scope inside here!
        var name = "Eve"
    }
 
    // logs "Eve", NOT "Adam", like languages with block scoping would do!
}

While the corresponding code in C++ works fine:

1
2
3
4
5
6
7
8
    char *name = "Adam";
 
    {   // new block scope
        char *name = "Eve";
    }
 
    // name is "Adam" again
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 
<h2>Nested scopes</h2>
 
For a better understanding, you can imagine JavaScript's scopes being "nested", so there are "outer" scopes and "inner" scopes, like the layers of an onion:
 
<pre lang="javascript" line="1">
// outermost, "global" layer, "surface" of the onion
function layer1() {
    // first layer below "global"
    var x;
    function layer2() {
        // second layer below "global", first below "layer1"
        var x;
 
        // furter "layers" for each "nested" function "inside" this function
    }
}

When JavaScript’s virtual machine tries to look an identifier up, means to figure out “which x is meant”, it always starts in the “current” execution context, in the “current” layer of “the onion skin”. If it can’t find the identifier in the given layer, it’ll change to the parent layer, and try there. This step repeats until the virtual machine reaches the “global”, the “top-most” layer of onion, the surface, so to say.

When do we leave a scope?

Well, that’s also stated in ECMA-262 standard chapter 10.4, “Establishing an Execution Context”, and it says:

Every return exits an execution context. A thrown exception may also exit one or more execution contexts.

Also: no magic involved. If you create a new Execution Context each time a function is being entered, “leaving” the function via return also leaves the corresponding Execution Context, thus, the local scope. And, as a thrown exception also leaves the function, it also leaves the Execution Context.

Three typical constellations

Yet, we can imagine three obvious, and typical, constellations for scopes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// as variable in global scope:
var x = 1;
 
var accessingGlobalScopeFromGlobalScope = x;
 
function accessingGlobalScopeFromLocalScope() {
    // new local scope, but x not defined in
    // local scope, so x will be resolved using
    // "outer scope", means "global scope"
 
    // logs "1":
    console.log(x);
 
    // modifies "x" in global scope:
    x = "bam!";
}
 
 
function accessingParameterInLocalScope(x) {
    // new local scope, and "x" is defined as
    // parameter, which will "hide" the global
    // variable "x"
 
    // logs whatever was given as parameter "x"
    console.log(x);
 
    // modifies local scope "x", but doesn't modify
    // global scope's "x"
    x = new Date();
}
 
 
function accessingLocalVariable() {
    // variable explicitly declared in local scope,
    // also "hides" global "x"-declaration:
    var x = "stahp";
 
    // logs "stahp":
    console.log(x);
 
    // modifies local scope "x", but doesn't modify
    // global scope's "x"
    x = "hurr,hurr";
}
 
accessingGlobalScopeFromLocalScope();
accessingParameterInLocalScope("da parameter!");
accessingLocalVariable();

Caution with non-locals in functions

This is a very common mistake, involving variables from non-local scopes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// some function we'll invoke later:
function logIt(str) {
    console.log(str);
}
 
// some todo-array, which we'll process later:
var todo = []
  , i;
 
// this code's intention was to dump the number 0..9 to
// the console. But it won't do so.
 
for(i=0; i<10; i++) {
    // put 10 elements in the array, all of type function,
    // which we can invoke later:
    todo.push(function() {
        // WRONG: we reference "i" from the outer scope,
        // and all functions share the same referenced value,
        // thus, all functions will reference the value "10"
        // after leaving this loop!
 
        logIt("I am number " + i);
    });
}
 
// now, let's invoke every function given in todo-array:
todo.forEach(function(fn) {
    fn.call();
});

What do you expect the upper code will write out to the console? It will be:

I am number 10
I am number 10
I am number 10
I am number 10
I am number 10
I am number 10
I am number 10
I am number 10
I am number 10
I am number 10

The explanation, involving the just learned term execution context, is, that in line 13 we actually put 10 functions into an array, which all hold a reference to the same variable named i. The term “same” refers to “the same variable in the same local scope”. Then, the loop increments i ten times, so finally, when we leave the loop, i will have the numeric value 10. Then, when we execute each of the ten functions, in line 21, every of the ten functions will look up i in the local scope now. And i is 10, so we actually log "I am number 10" ten times.

What do we need to get the expected behaviour, so we get logged output of shape "I am number 0" upto "I am number 9"? Well, we need to ensure i won’t be placed as a reference in the same scope over and over again, but to put it in a new scope for each callback. This could be done like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ...
 
for(i=0; i<10; i++) {
    // possibility #1: create new scope by closure, pass
    // current value as parameter "index", NOT REALLY A GOOD PRACTISE,
    // but does the trick, BETTER ALTERNATIVE TO FOLLOW:
    (function(index) {
        // now, we placed the "current" value of "i" into a new local scope,
        // by passing it's current value as a parameter named "index"
        todo.push(function() {
            logIt("I am number " + index);
        });
    })(i);
}
 
// ...

Another possibilty, just for the sake of completeness, not because it’s a good practise, of what we just learned: we assign the value to a variable in the new local scope:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ...
 
for(i=0; i<10; i++) {
    // possibility #2: create new scope by closure, assign
    // current value of "i" to a new, local scoped variable,
    // also NOT REALLY A GOOD PRACTISE, but does the trick,
    // BETTER ALTERNATIVE TO FOLLOW:
    (function() {
        var index = i;
 
        // now, we placed the "current" value of "i" into a new local scope,
        // by assigning it's current value to a variable named "index":
        todo.push(function() {
            logIt("I am number " + index);
        });
    })();
}
 
// ...

So, what’s all about that “not a good practise”? Well, creating functions in a loop often results in a bad performance, like in the upper two examples, because we instantiate a new closure-function in each iteration of the for-loop. Still remember the “when do we enter a new execution context”-thing? Yes? Remember, it said:

Every invocation of an ECMAScript code function (13.2.1) also establishes and enters a new execution context

See the point? It would be sufficient to invoke a function for creating a new scope – but we actually instantiate and invoke a new closure each iteration. Thus, a better approach would be to simply “recycle” one (and the same) given function (instance) over and over again, by simply wrapping the “push a new function into the todo-array” in a function, and then calling that from within our loop:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
var todo = []
  , i;
 
function logIt(str) {
    console.log(str);
}
 
function pushLogIt(index, todo) {
    // new inner scope, so "index" holds "current time value" of "i"
    todo.push(function() {
        logIt("I am number " + index);
    });
}
 
 
for(i=0; i<10; i++) {
    // much better: invoke function for creating new execution context
    // and "preserving current value of i" in a parameter value, saves
    // the cost of instantiating a new closure function each iteration:
    pushLogIt(i, todo);
}
 
// now, let's invoke every function given in todo-array:
todo.forEach(function(fn) {
    fn.call();
});
 
/*
// Now logs:
 
I am number 0
I am number 1
I am number 2
I am number 3
I am number 4
I am number 5
I am number 6
I am number 7
I am number 8
I am number 9
*/

Functions anywhere

What possibilities do we have to create “functions”? Three possibilities come to my mind:

  1. the function-statement
  2. the function-operator
  3. the Function-object

An example using the function-statement:

1
2
3
function iUseTheFunctionStatement() {
    // ...
} // note: NO semicolon

An example using the function-operator:

1
2
3
var iUseTheFunctionOperator = function() {
    // ...
}; // note: SEMICOLON, because terminates assignment

An example using the Function-Object:

1
var increment = new Function("x", "return x+1");

Summary

Today, we’ve seen that:

  • we create new execution contexts/scopes each time we enter a function by invocation
  • we leave an execution context/scope each time we leave the corresponding function, either by return or by throw
  • it’s sufficient to use one function by invocation, we don’t need to instantiate closure-functions over and over again

Further information

  1. ECMA-262, Section 10, Executable Code and Execution Contexts
  2. David Shariff about Identifier Resolution and Closures in the JavaScript Scope Chain
  3. Function-object docs at Mozilla Developer Network
  4. function-operator docs at Mozilla Developer Network
  5. function-statement docs at Mozilla Developer Network
Tagged with: , ,
Posted in JavaScript

Leave a Reply

Your email address will not be published. Required fields are marked *

*