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()”