JavaScript for impatient programmers (beta)
Please support this book: buy it or donate
(Ad, please don’t block.)

21. Variable scopes and closures (advanced)

In this chapter, we cover in more depth, how JavaScript handles variables. There are six ways of declaring variables: var, let, const, function, class and import. We initially explore let and const and later apply what we have learned to the remaining constructs.

21.1. The scope of a variable

The scope of a variable is the part of the program in which the variable exists. Consider the following code.

{ // (A)
  let x = 0;
  // This scope: can access x
  { // (B)
    let y = 1;
    // This scope: can access x, y
    { // (C)
      let z = 2;
      // This scope: can access x, y, z
    }
  }
}
// Outside: can’t access x, y, z

The scope of a variable declared via const or let is always the directly surrounding block. That’s why these declarations are called block-scoped.

21.2. Terminology: static versus dynamic

These two adjectives describe phenomena in programming languages:

Let’s look at examples for these two terms.

21.2.1. Static phenomenon: scopes of variables

Variable scopes are a static phenomenon. Consider the following code:

function f() {
  const x = 3;
  // ···
}

x is statically (or lexically) scoped. That is, its scope is fixed and doesn’t change at runtime.

Variable scopes form a static tree (via static nesting).

21.2.2. Dynamic phenomenon: function calls

Function calls are a dynamic phenomenon. Consider the following code:

function g(x) {}
function h(y) {
  if (Math.random()) g(y); // (A)
}

Whether or not the function call in line A happens, can only be decided at runtime.

Function calls form a dynamic tree (via dynamic calls).

21.3. Temporal dead zone (TDZ)

Declaring a variable, influences its whole scope, because you can’t declare the same variable twice (directly) inside the same scope.

Thus, for JavaScript, TC39 needed to decide what happens if you enter a new scope and mention the name of a variable before its declaration. Two possible approaches are:

TC39 chose the latter for const and let, because you likely made a mistake, if you use a name that is declared later in the same scope.

The time between entering the scope of a variable and executing its declaration is called the temporal dead zone of that variable:

The following code illustrates the temporal dead zone:

if (true) { // enter scope of `tmp`, TDZ starts
  // `tmp` is uninitialized:
  assert.throws(() => (tmp = 'abc'), ReferenceError);
  assert.throws(() => console.log(tmp), ReferenceError);

  let tmp; // TDZ ends
  assert.equal(tmp, undefined);

  tmp = 123;
  assert.equal(tmp, 123);
}

The next example shows that the temporal dead zone is truly temporal (related to time):

if (true) { // enter scope of `myVar`, TDZ starts
  const func = () => {
    console.log(myVar); // executed later
  };

  // We are within the TDZ:
  // Accessing `myVar` causes `ReferenceError`

  let myVar = 3; // TDZ ends
  func(); // OK, called outside TDZ
}

Even though func() is located before the declaration of myVar and uses that variable, we can call func(). But we have to wait until the temporal dead zone of myVar is over.

21.4. Hoisting

Hoisting means that a construct is moved to the beginning of its scope, regardless of where it is located in that scope. We have already seen hoisting in the chapter on callable entities:

assert.equal(func(), 123); // Works!

function func() {
  return 123;
}

You can use func() before its declaration, because, internally, it is hoisted. That is, the previous code is actually executed like this:

function func() {
  return 123;
}

assert.equal(func(), 123);

It’s nice that we can use a function before declaring it. That gives us more freedom in structuring our code.

The temporal dead zone can also be viewed as a form of hoisting, because the declaration affects what happens at the beginning of its scope.

21.5. Global variables

A variable is global if it is declared in the top-level scope. Every nested scope can access such a variable. In JavaScript, there are multiple layers of global scopes (Fig. 6):

Figure 6: JavaScript has multiple global scopes.
Figure 6: JavaScript has multiple global scopes.

21.5.1. The global object

The global object lets you access the outermost global scope via an object. The two are always in sync:

The global object is available via special variables:

Let’s examine how self works:

// At the top level of a script
var myGlobalVariable = 123;
assert.ok('myGlobalVariable' in self);

delete self.myGlobalVariable;
assert.throws(() => console.log(myGlobalVariable), ReferenceError);

// Create a global variable anywhere:
if (true) {
  self.anotherGlobalVariable = 'abc';
}
assert.equal(anotherGlobalVariable, 'abc');

21.5.2. Avoid the global object!

Brendan Eich called the global object one of his biggest regrets about JavaScript. It is best not to put variables into its scope:

You occasionally see window.globalVariable in tutorials on the web, but the prefix “window.” is not necessary. I prefer to omit it:

window.encodeURIComponent(str); // no
encodeURIComponent(str); // yes

21.6. Ways of declaring variables

Table 19: These are all the ways in which you declare variables in JavaScript.
Hoisting Scope Script scope is global object?
var Declaration only Function
let Temporal dead zone Block
const Temporal dead zone Block
function Everything Block
class No Block
import Everything Module

Tbl. 19 lists all ways in which you can declare variables in JavaScript: var, let, const, function, class and import.

21.7. Variables in detail: environments

Environments are the data structure that JavaScript engines use to manage variables. You need to know how they work if you want to understand an important advanced JavaScript concept: closures.

An environment is a dictionary whose keys are variable names and whose values are the values of those variables. Each scope has its associated environment. Environments must be able to support the following phenomena related to variables:

We’ll use examples to illustrate how that is done for each phenomenon.

21.8. Recursion via environments

We’ll tackle recursion first. Consider the following code:

function f(x) {
  return x * 2;
}
function g(y) {
  const tmp = y + 1;
  return f(tmp);
}
assert.equal(g(3), 8);

For each function call, you need fresh storage space for the variables (parameters and local variables) of the called function. This is managed via a stack of so-called execution contexts, which – for the purpose of this chapter, are references to environments. Environments themselves are stored on the heap and can therefore be shared via references (more than one storage location can point to them).

21.8.1. Executing the code

While executing the code, we make the following pauses:

function f(x) {
  // Pause 3
  return x * 2;
}
function g(y) {
  const tmp = y + 1;
  // Pause 2
  return f(tmp);
}
// Pause 1
assert.equal(g(3), 8);

This is what happens:

Figure 7: Recursion, pause 1 – before calling g(): The execution context stack has one entry, which points to the global environment. In that environment, there are two entries; one for f() and one for g().
Figure 7: Recursion, pause 1 – before calling g(): The execution context stack has one entry, which points to the global environment. In that environment, there are two entries; one for f() and one for g().
Figure 8: Recursion, pause 2 – while executing g(): The top of the execution context stack points to the environment that was created for g(). That environment contains entries for the argument y and for the local variable tmp.
Figure 8: Recursion, pause 2 – while executing g(): The top of the execution context stack points to the environment that was created for g(). That environment contains entries for the argument y and for the local variable tmp.
Figure 9: Recursion, pause 3 – while executing f(): The top execution context now points to the environment for f().
Figure 9: Recursion, pause 3 – while executing f(): The top execution context now points to the environment for f().

21.9. Nested scopes via environments

We use the following code to explore how nested scopes are implemented via environments.

function f(x) {
  function square() {
    const result = x * x;
    return result;
  }
  return square();
}
assert.equal(f(6), 36);

The idea is that the environment of each scope points to the environment of the surrounding scope, via a field called outer. That is, the latter environment is the outer environment of the former environment. When we are looking up the value of a variable, we first search for its name in the current environment, then in the outer environment, then in the outer environment’s outer environment, etc. The whole chain of outer environments contains all variables that can currently be accessed (if we are ignoring shadowed variables).

When you make a function call, you create a new environment. The outer environment of that environment is the environment in which the function is stored – that’s how static scoping works. To help set up the field outer of environments created via function calls, each function has an internal property named [[Scope]] that points to its “birth environment”.

21.9.1. Executing the code

These are the pauses we are making while executing the code:

function f(x) {
  function square() {
    const result = x * x;
    // Pause 3
    return result;
  }
  // Pause 2
  return square();
}
// Pause 1
assert.equal(f(6), 36);

This is what happens:

Figure 10: Nested scopes, pause 1 – before calling f(): The global environment has a single entry, for f(). The birth environment of f() is the global environment. Therefore, f’s [[Scope]] points to it.
Figure 10: Nested scopes, pause 1 – before calling f(): The global environment has a single entry, for f(). The birth environment of f() is the global environment. Therefore, f’s [[Scope]] points to it.
Figure 11: Nested scopes, pause 2 – while executing f(): There is now an environment for the function call f(6). The outer environment of that environment is the birth environment of f() (the global environment at index 0). We can see that the field outer was set to the value of f’s [[Scope]]. Furthermore, the [[Scope]] of the new function square() is the environment that was just created.
Figure 11: Nested scopes, pause 2 – while executing f(): There is now an environment for the function call f(6). The outer environment of that environment is the birth environment of f() (the global environment at index 0). We can see that the field outer was set to the value of f’s [[Scope]]. Furthermore, the [[Scope]] of the new function square() is the environment that was just created.
Figure 12: Nested scopes, pause 3 – while executing square(): The previous pattern was repeated: the outer of the most recent environment was set up via the [[Scope]] of the function that we just called. The chain of scopes created via outer, contains all variables that are active right now. For example, we can access result, square and f, if we want to. Environments reflect two aspects of variables. First, the chain of outer environments reflects the nested static scopes. Second, the stack of execution contexts reflects what function calls were made, dynamically.
Figure 12: Nested scopes, pause 3 – while executing square(): The previous pattern was repeated: the outer of the most recent environment was set up via the [[Scope]] of the function that we just called. The chain of scopes created via outer, contains all variables that are active right now. For example, we can access result, square and f, if we want to. Environments reflect two aspects of variables. First, the chain of outer environments reflects the nested static scopes. Second, the stack of execution contexts reflects what function calls were made, dynamically.

21.10. Closures

Before we can explore closures, we need to learn about bound variables and free variables.

21.10.1. Bound variables versus free variables

Per scope, there is a set of variables that are mentioned. Among these variables we distinguish:

Consider the following code:

function func(x) {
  const y = 123;
  console.log(z);
}

Within (the scope of) func(), x and y are bound variables. z is a free variable.

21.10.2. What is a closure?

So what is a closure?

A closure is a function plus its “birth environment” (the environment that was active when the function was created).

What is the point of packaging the two? The birth environment provides the values for the free variables of the function. For example:

function funcFactory(value) {
  return function () {
    return value;
  };
}

const func = funcFactory('abc');
assert.equal(func(), 'abc'); // (A)

funcFactory returns a closure that is assigned to func. Because func has the connection to the variables at its birth place, it can still access the free variable value when it is called in line A (even though it “escaped” its scope).

All functions in JavaScript are closures. We saw that when we were looking at how environments support nested scopes: Each function has the internal property [[Scope]] that points to its birth environment.

21.10.3. Example: Currying add()

The following code demonstrates another closure:

function add(x) {
  return function (y) { // (A)
    return x + y;
  };
}
assert.equal(add(3)(1), 4); // (B)

What is going on here? add() is a function that returns a function. When we make the nested function call add(3)(1) in line B, the first parameter is for add(), the second parameter is for the function it returns. This trick works, because the function created in line A does not lose the connection to its birth scope when it leaves that scope. The associated environment is kept alive by that connection and the function still has access to variable x in that environment (x is free inside the function).

This nested way of calling add() has an advantage: If you only make the first function call, you get a version of add() whose parameter x is already filled in:

const plus2 = add(2);
assert.equal(plus2(5), 7);

Converting a function with two parameters into two nested functions with one parameter each, is called currying. add() is a curried function.

Only filling in some of the parameters of a function is called partial application (the function has not been fully applied, yet). Method .bind() of functions performs partial application. In the previous example, we can see that partial application is simple if a function is curried.

21.10.3.1. Executing the code

As we are executing the following code, we are making three pauses:

function add(x) {
  return function (y) {
    // Pause 3: plus2(5)
    return x + y;
  }; // Pause 1: add(2)
}
const plus2 = add(2);
// Pause 2
assert.equal(plus2(5), 7);

This is what happens:

Figure 13: Closures, pause 1 – during the execution of add(2): We can see that the function returned by add() already exists (see bottom right corner) and that it points to its birth environment via its internal property [[Scope]]. Note that plus2 is still in its temporal dead zone and uninitialized.
Figure 13: Closures, pause 1 – during the execution of add(2): We can see that the function returned by add() already exists (see bottom right corner) and that it points to its birth environment via its internal property [[Scope]]. Note that plus2 is still in its temporal dead zone and uninitialized.
Figure 14: Closures, pause 2 – after the execution of add(2): plus2 now points to the function returned by add(2). That function keeps its birth environment (the environment of add(2)) alive via its [[Scope]].
Figure 14: Closures, pause 2 – after the execution of add(2): plus2 now points to the function returned by add(2). That function keeps its birth environment (the environment of add(2)) alive via its [[Scope]].
Figure 15: Closures, pause 3 – while executing plus2(5): The [[Scope]] of plus2(5) is used to set up the outer of the new environment. That’s how the current function gets access to x.
Figure 15: Closures, pause 3 – while executing plus2(5): The [[Scope]] of plus2(5) is used to set up the outer of the new environment. That’s how the current function gets access to x.

21.10.4. Example: A factory for incrementors

The following function returns incrementors (a name that I just invented). An incrementor is a function that internally stores a number. When it is called, it updates that number by adding the argument to it and returns the new value.

function createInc(startValue) {
  return function (step) { // (A)
    startValue += step;
    return startValue;
  };
}
const inc = createInc(5);
assert.equal(inc(2), 7);

We can see that the function created in line A keeps its internal number in the free variable startValue. This time, we don’t just read from the birth environment, we use it to store data that we change and that persists across function calls.

We can create more storage slots in the birth environment, via local variables:

function createInc(startValue) {
  let index = -1;
  return function (step) {
    startValue += step;
    index++;
    return [index, startValue];
  };
}
const inc = createInc(5);
assert.deepEqual(inc(2), [0, 7]);
assert.deepEqual(inc(2), [1, 9]);
assert.deepEqual(inc(2), [2, 11]);

21.10.5. Use cases for closures

So what are closures good for?

  Quiz

See quiz app.