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.
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:
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 that maps variable names to variable values (think dictionary). This is the actual storage space for the variables of the scope. The name-value entries in the record are called bindings.
A reference to the outer environment – the environment for the outer scope.
The tree of nested scopes is therefore represented by a tree of environments linked by outer environment references.
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:
globalThis
. The name is based on the fact that it has the same value as this
in global scope.window
is the classic way of referring to the global object. It works in normal browser code, but not in Web Workers (processes running concurrently to the normal browser process) and not on Node.js.self
is available everywhere in browsers (including in Web Workers). But it isn’t supported by Node.js.global
is only available on Node.js.globalThis
does not point directly to the global objectIn browsers, globalThis
does not point directly to the global, there is an indirection. As an example, consider an iframe on a web page:
src
of the iframe changes, it gets a new global object.globalThis
always has the same value. That value can be checked from outside the iframe, as demonstrated below (inspired by an example in the globalThis
proposal).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
:
How do browsers ensure that globalThis
doesn’t change in this scenario? They internally distinguish two objects:
Window
is the global object. It changes whenever the location changes.WindowProxy
is an object that forwards all accesses to the current Window
. This object never changes.In browsers, globalThis
refers to the WindowProxy
; everywhere else, it directly refers to the global object.
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:
An object environment record has the same interface as a normal environment record, but keeps its bindings in a JavaScript object. In this case, the object is the global object.
A normal (declarative) environment record that has its own storage for its bindings.
Which of these two records is used when will be explained soon.
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)
}
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:
const
, let
, and class
create bindings in the declarative environment record.var
and function declarations create bindings in the object environment 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(globalThis.one); // undefined
console.log(globalThis.two); // 2
</script>
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>
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. 10 summarizes everything we have learned in this section.
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.
Environments and the global object in the ECMAScript specification:
globalThis
:
globalThis
”this
value: “A horrifying globalThis
polyfill in universal JavaScript” by Mathias BynensThe global object in browsers:
this
: section “InitializeHostDefinedRealm()”