How do JavaScript’s global variables really work?

[2019-07-07] dev, javascript, es feature
(Ad, please don’t block)

In this blog post, we examine how JavaScript’s global variables work. Several interesting phenomena play a role: the scope of scripts, the so-called global object, and more.

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 foo = 1;
  if (true) { // (B)
    const bar = 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.

Lexical environments  

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

  • An environment record (think dictionary) that maps variable names to variable values. This is where JavaScript stores variables. One key-value entry in the environment record is called a binding.

  • A reference to the outer environment – the environment representing the outer scope of the scope represented by the current environment.

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

The global object  

The global object is an object whose properties are global variables. (We’ll examine soon how exactly it fits into the tree of environments.) It has several different names:

  • Everywhere (proposed feature): globalThis
  • Other names for the global object depend on platform and language construct:
    • window: is the classic way of referring to the global object. But it only works in normal browser code; not in Node.js and not in Web Workers (processes running concurrently to normal browser code).
    • self: is available everywhere in browsers, including in Web Workers. But it isn’t supported by Node.js.
    • global: is only available in Node.js.

The global object contains all built-in global variables.

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 references. The outer reference of the global environment is null.

The global environment combines two environment records:

  • An object environment record that works like a normal environment record, but keeps its bindings in sync with an object. In this case, the object is the global object.
  • A normal (declarative) environment record.

The following diagram shows these data structures. Script scope and module environments are explained soon.

The next two subsections explain how the object record and the declarative record are combined.

Creating variables  

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

  • Top-level const, let, and class create bindings in the declarative record.
  • Top-level var and function declarations create bindings in the object record.
<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(window.one); // undefined
  console.log(window.two); // 2
</script>

Additionally, the global object contains all built-in global variables and contributes them to the global environment via the object record.

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 foo = 1; // declarative environment record
  globalThis.foo = 2; // object environment record

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

Module environments  

Each module has its own environment. It stores all top-level declarations – including imports. The outer environment of a module environment is the global environment.

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.

Further reading