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

4 A detailed look at global variables



In this chapter, we take a detailed look at how JavaScript’s global variables work. Several interesting phenomena play a role: the scope of scripts, the so-called global object, and more.

4.1 Scopes

The lexical scope (short: scope) of a variable is the region of a program where it can be accessed. JavaScript’s scopes are static (they don’t change at runtime) and they can be nested – for example:

function func() { // (A)
  const aVariable = 1;
  if (true) { // (B)
    const anotherVariable = 2;
  }
}

The scope introduced by the if statement (line B) is nested inside the scope of function func() (line A).

The innermost surrounding scope of a scope S is called the outer scope of S. In the example, func is the outer scope of if.

4.2 Lexical environments

In the JavaScript language specification, scopes are “implemented” via lexical environments. They consist of two components:

The tree of nested scopes is therefore represented by a tree of environments linked by outer environment references.

4.3 The global object

The global object is an object whose properties become global variables. (We’ll examine soon how exactly it fits into the tree of environments.) It can be accessed via the following global variables:

4.4 In browsers, globalThis does not point directly to the global object

In browsers, globalThis does not point directly to the global, there is an indirection. As an example, consider an iframe on a web page:

File parent.html:

<iframe src="iframe.html?first"></iframe>
<script>
  const iframe = document.querySelector('iframe');
  const icw = iframe.contentWindow; // `globalThis` of iframe

  iframe.onload = () => {
    // Access properties of global object of iframe
    const firstGlobalThis = icw.globalThis;
    const firstArray = icw.Array;
    console.log(icw.iframeName); // 'first'

    iframe.onload = () => {
      const secondGlobalThis = icw.globalThis;
      const secondArray = icw.Array;

      // The global object is different
      console.log(icw.iframeName); // 'second'
      console.log(secondArray === firstArray); // false

      // But globalThis is still the same
      console.log(firstGlobalThis === secondGlobalThis); // true
    };
    iframe.src = 'iframe.html?second';
  };
</script>

File iframe.html:

<script>
  globalThis.iframeName = location.search.slice(1);
</script>

How do browsers ensure that globalThis doesn’t change in this scenario? They internally distinguish two objects:

In browsers, globalThis refers to the WindowProxy; everywhere else, it directly refers to the global object.

4.5 The global environment

The global scope is the “outermost” scope – it has no outer scope. Its environment is the global environment. Every environment is connected with the global environment via a chain of environments that are linked by outer environment references. The outer environment reference of the global environment is null.

The global environment record uses two environment records to manage its variables:

Which of these two records is used when will be explained soon.

4.5.1 Script scope and module scopes

In JavaScript, we are only in global scope at the top levels of scripts. In contrast, each module has its own scope that is a subscope of the script scope.

If we ignore the relatively complicated rules for how variable bindings are added to the global environment, then global scope and module scopes work as if they were nested code blocks:

{ // Global scope (scope of *all* scripts)

  // (Global variables)

  { // Scope of module 1
    ···
  }
  { // Scope of module 2
    ···
  }
  // (More module scopes)
}

4.5.2 Creating variables: declarative record vs. object record

In order to create a variable that is truly global, we must be in global scope – which is only the case at the top level of scripts:

<script>
  const one = 1;
  var two = 2;
</script>
<script>
  // All scripts share the same top-level scope:
  console.log(one); // 1
  console.log(two); // 2
  
  // Not all declarations create properties of the global object:
  console.log(globalThis.one); // undefined
  console.log(globalThis.two); // 2
</script>

4.5.3 Getting or setting variables

When we get or set a variable and both environment records have a binding for that variable, then the declarative record wins:

<script>
  let myGlobalVariable = 1; // declarative environment record
  globalThis.myGlobalVariable = 2; // object environment record

  console.log(myGlobalVariable); // 1 (declarative record wins)
  console.log(globalThis.myGlobalVariable); // 2
</script>

4.5.4 Global ECMAScript variables and global host variables

In addition to variables created via var and function declarations, the global object contains the following properties:

Using const or let guarantees that global variable declarations aren’t influencing (or influenced by) the built-in global variables of ECMAScript and host platform.

For example, browsers have the global variable .location:

// Changes the location of the current document:
var location = 'https://example.com';

// Shadows window.location, doesn’t change it:
let location = 'https://example.com';

If a variable already exists (such as location in this case), then a var declaration with an initializer behaves like an assignment. That’s why we get into trouble in this example.

Note that this is only an issue in global scope. In modules, we are never in global scope (unless we use eval() or similar).

Fig. 1 summarizes everything we have learned in this section.

Figure 1: The environment for the global scope manages its bindings via a global environment record which in turn is based on two environment records: an object environment record whose bindings are stored in the global object and a declarative environment record that uses internal storage for its bindings. Therefore, global variables can be created by adding properties to the global object or via various declarations. The global object is initialized with the built-in global variables of ECMAScript and the host platform. Each ECMAScript module has its own environment whose outer environment is the global environment.

4.6 Conclusion: Why does JavaScript have both normal global variables and the global object?

The global object is generally considered to be a mistake. For that reason, newer constructs such as const, let, and classes create normal global variables (when in script scope).

Thankfully, most of the code written in modern JavaScript, lives in ECMAScript modules and CommonJS modules. Each module has its own scope, which is why the rules governing global variables rarely matter for module-based code.

4.7 Further reading and sources of this chapter

Environments and the global object in the ECMAScript specification:

globalThis:

The global object in browsers: