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.
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
.
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 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:
globalThis
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 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:
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.
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:
const
, let
, and class
create bindings in the declarative record.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.
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>
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.
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.
globalThis
”