The globalThis
proposal introduces a unified mechanism to access the global this
in any JavaScript environment. It sounds like a simple thing to polyfill, but it turns out it’s pretty hard to get right. I didn’t even think it was possible until Toon blew my mind with an unexpected, creative solution.
This write-up describes the difficulties with writing a proper globalThis
polyfill. Such a polyfill has the following requirements:
- It must work in any JavaScript environment, including web browsers, workers in web browsers, extensions in web browsers, Node.js, Deno, and standalone JavaScript engine binaries.
- It must support sloppy mode, strict mode, and JavaScript modules.
- It must work regardless of the context the code runs in. (That is, it must still produce the correct result even if the polyfill is wrapped in a strict mode function by a packer at build time.)
Terminology
But first, a note on terminology. globalThis
provides the value of this
in the global scope. This is different from the global object in web browsers, for complicated reasons.
Note that in JavaScript modules, there is a module scope intervening between the global scope and your code. The module scope hides the global scope’s this
value, so the this
value you see at the top-level in modules is actually undefined
.
TL;DR globalThis
is not “the global object”; it’s simply the this
from the global scope. Thanks to Domenic for helping me understand this important nuance.
globalThis
alternatives
In a browser, globalThis
is equivalent to window
:
globalThis === window;
// → true
frames
works too:
globalThis === frames;
// → true
However, window
and frames
are undefined
within worker contexts (such as web workers and service workers). Luckily, self
works in all browser contexts and is thus a more robust alternative:
globalThis === self;
// → true
Neither window
, frames
, nor self
are available within Node.js, though. Instead, global
can be used:
globalThis === global;
// → true
None of the above (window
, frames
, self
, global
) are available in stand-alone JavaScript engine shells, such as the ones installed by jsvu
. There, you can access the global this
:
globalThis === this;
// → true
Furthermore, sloppy mode functions always have their this
set to the global this
, so even if you cannot run your code in the global scope, you could still get access to the global this
as follows in sloppy mode:
globalThis === (function() {
return this;
})();
// → true
However, the top-level this
value is undefined
in JavaScript modules, and this
is undefined
within strict mode functions, so this approach doesn’t work there.
Once you’re in a strict mode context, there’s only one way to temporarily break out of it: the Function
constructor, which generates sloppy functions!
globalThis === Function('return this')();
// → true
Well, there’s two ways, since indirect eval
has the same effect:
globalThis === (0, eval)('this');
// → true
In web browsers, use of the Function
constructor and eval
is often disallowed using Content Security Policy (CSP). Websites often opt-in so such a policy, but it’s also enforced within Chrome extensions, for example. Unfortunately, this means that a proper polyfill cannot rely on the Function
constructor or eval
.
Note: setTimeout('globalThis = this', 0)
is ruled out for the same reason. In addition to commonly being blocked by CSP, there are two other reasons against using setTimeout
for a polyfill. First, it’s not part of ECMAScript, and not available in all JavaScript environments. Second, it’s asynchronous, so even if setTimeout
would be supported everywhere, it’d be painful to use in a polyfill on which other code depends.
A naive polyfill
It seems like it should be possible to combine the above techniques into a single polyfill, like so:
// A naive globalThis shim. Don’t use this!
const getGlobalThis = () => {
if (typeof globalThis !== 'undefined') return globalThis;
if (typeof self !== 'undefined') return self;
if (typeof window !== 'undefined') return window;
if (typeof global !== 'undefined') return global;
if (typeof this !== 'undefined') return this;
throw new Error('Unable to locate global `this`');
};
// Note: `var` is used instead of `const` to ensure `globalThis`
// becomes a global variable (as opposed to a variable in the
// top-level lexical scope) when running in the global scope.
var globalThis = getGlobalThis();
But alas, this doesn’t work in strict mode functions or within JavaScript modules in non-browser environments (except those with globalThis
support). In addition, getGlobal
could return an incorrect result, since it relies on this
which is context-dependent and could be altered by a bundler/packer.
A robust polyfill
Is it even possible to write a robust globalThis
polyfill? Assume an environment where:
- you cannot rely on the value of
globalThis
,window
,self
,global
, orthis
; - you cannot use the
Function
constructor oreval
; - but you can rely on the integrity of all other JavaScript built-in functionality.
It turns out there is a solution, but it’s not pretty. Let’s think about this for a minute.
How do we get access to the global this
without knowing how to access it directly? If we could somehow install a function property on the globalThis
, and call it as a method on the globalThis
, then we could access the this
from that function:
globalThis.foo = function() {
return this;
};
var globalThisPolyfilled = globalThis.foo();
How can we do something like that without relying on globalThis
or any host-specific binding that refers to it? We can’t just do the following:
function foo() {
return this;
}
var globalThisPolyfilled = foo();
foo()
is now no longer called as a method, and so its this
is undefined
in strict mode or in JavaScript modules as discussed above. Strict mode functions have their this
set to undefined
. However, this is not the case for getters and setters!
Object.defineProperty(globalThis, '__magic__', {
get: function() {
return this;
},
configurable: true // This makes it possible to `delete` the getter later.
});
// Note: `var` is used instead of `const` to ensure `globalThis`
// becomes a global variable (as opposed to a variable in the
// top-level lexical scope) when run in the global scope.
var globalThisPolyfilled = __magic__;
delete globalThis.__magic__;
The above program installs a getter on the globalThis
, accesses the getter to get a reference to the globalThis
, and then cleans up by deleting the getter. This technique gives us access to the globalThis
in all the desired circumstances, but it still relies on a reference to the global this
on the first line (where it says globalThis
). Can we avoid this dependency? How can we install a globally accessible getter without accessing the globalThis
directly?
Instead of installing the getter on globalThis
, we install it on something the global this
object inherits from — Object.prototype
:
Object.defineProperty(Object.prototype, '__magic__', {
get: function() {
return this;
},
configurable: true // This makes it possible to `delete` the getter later.
});
// Note: `var` is used instead of `const` to ensure `globalThis`
// becomes a global variable (as opposed to a variable in the
// top-level lexical scope).
var globalThis = __magic__;
delete Object.prototype.__magic__;
Note: Prior to the globalThis
proposal, the ECMAScript spec doesn’t actually mandate that the global this
inherit from Object.prototype
, only that it must be an object. Object.create(null)
creates an object that doesn’t inherit from Object.prototype
. A JavaScript engine could use such an object as the global this
without violating the spec, in which case the above code snippet still wouldn’t work (and indeed, Internet Explorer 7 did something like that!). Luckily, more modern JavaScript engines all seem to agree that the global this
must have Object.prototype
in its prototype chain.
To avoid mutating Object.prototype
in modern environments where globalThis
is already available, we can change the polyfill as follows:
(function() {
if (typeof globalThis === 'object') return;
Object.defineProperty(Object.prototype, '__magic__', {
get: function() {
return this;
},
configurable: true // This makes it possible to `delete` the getter later.
});
__magic__.globalThis = __magic__; // lolwat
delete Object.prototype.__magic__;
}());
// Your code can use `globalThis` now.
console.log(globalThis);
Alternatively, we could use __defineGetter__
:
(function() {
if (typeof globalThis === 'object') return;
Object.prototype.__defineGetter__('__magic__', function() {
return this;
});
__magic__.globalThis = __magic__; // lolwat
delete Object.prototype.__magic__;
}());
// Your code can use `globalThis` now.
console.log(globalThis);
And there you have it: the most horrifying polyfill you’ve ever seen! It completely defies the prevalent best practice to not modify objects you don’t own. Mucking with built-in prototypes is generally a bad idea, as explained in JavaScript engine fundamentals: optimizing prototypes.
On the other hand, the only way this polyfill can break is if someone manages to alter Object
or Object.defineProperty
(or Object.prototype.__defineGetter__
) before the polyfill code runs. I can’t think of a more robust solution. Can you?
Testing the polyfill
The polyfill is a good an interesting example of universal JavaScript: it’s pure JavaScript code that does not rely on any host-specific built-ins, and therefore runs in any environment that implements ECMAScript. This was one of the goals of the polyfill in the first place! Let’s confirm that it actually works.
Here’s an HTML demo page for the polyfill that logs globalThis
using both a classic script globalthis.js
and a module globalthis.mjs
(with identical source code). This demo can be used to verify the polyfill works in browsers. globalThis
is natively supported in V8 v7.1 / Chrome 71, Firefox 65, Safari 12.1, and iOS Safari 12.2. To test the interesting parts of the polyfill, open the demo page in an older browser.
Note: The polyfill does not work in Internet Explorer 10 and older. In those browsers, the line __magic__.globalThis = __magic__
somehow doesn’t make globalThis
globally available, despite __magic__
being a working reference to the global this
. It turns out __magic__ !== window
although both are [object Window]
, indicating that these browsers might be confused about the distinction between the global object and the global this
. Amending the polyfill to fall back to one of the alternatives makes it work in IE 10 and IE 9. For IE 8 support, wrap the call to Object.defineProperty
in a try
-catch
, similarly falling back in the catch
block. (Doing so also avoids the IE 7 issue with the global this
not inheriting from Object.prototype
.) Try the demo with old IE support.
To test in Node.js and standalone JavaScript engine binaries, download the very same JavaScript files:
# Download the polyfill + demo code as a module.
curl https://mathiasbynens.be/demo/globalthis.mjs > globalthis.mjs
# Create a copy (well, symlink) of the file, to be used as a classic script.
ln -s globalthis.mjs globalthis.js
Now we can test in node
:
$ node globalthis.mjs
Testing the polyfill in a module
[object global]
$ node globalthis.js
Testing the polyfill in a classic script
[object global]
To test in a stand-alone JavaScript engine shell, use jsvu
to install any desired engine, and then run the scripts directly. For example, to test in V8 v7.0 (without globalThis
support) and v7.1 (with globalThis
support):
$ jsvu v8@7.0 # Install the `v8-7.0.276` binary.
$ v8-7.0.276 globalthis.mjs
Testing the polyfill in a module
[object global]
$ v8-7.0.276 globalthis.js
Testing the polyfill in a classic script
[object global]
$ jsvu v8@7.1 # Install the `v8-7.1.302` binary.
$ v8-7.1.302 globalthis.js
Testing the polyfill in a classic script
[object global]
$ v8-7.1.302 globalthis.mjs
Testing the polyfill in a module
[object global]
The same technique allows us to test in JavaScriptCore, SpiderMonkey, Chakra, and other JavaScript engines such as XS as well. Here’s an example using JavaScriptCore:
$ jsvu # Install the `javascriptcore` binary.
$ javascriptcore globalthis.mjs
Testing the polyfill in a module
[object global]
$ javascriptcore globalthis.js
Testing the polyfill in a classic script
[object global]
Conclusion
Writing universal JavaScript can be tricky, and often calls for creative solutions. The new globalThis
feature makes it easier to write universal JavaScript that needs access to the global this
value. Polyfilling globalThis
correctly is more challenging than it seems, but there is a working solution.
Only use this polyfill when you really need to. JavaScript modules make it easier than ever to import and export functionality without altering global state, and most modern JavaScript code doesn’t need access to the global this
anyway. globalThis
is only useful for libraries and polyfills that do.
globalThis
polyfills on npm
Since publishing this article, the following npm packages started providing globalThis
polyfills that make use of this technique:
Disclaimer: I’m not the author, nor am I the maintainer, of any of these packages.
Comments
MaxArt wrote on :
The polyfill itself doesn’t faze me. It’s a polyfill, it can do the worst to reach the goal. End users don’t need to know the details to use it.
TRWTF in this article is IMO this:
(0, eval)('this')
.Seriously, wut? Isn’t this equivalent to just
eval('this')
? Why the comma operator? 🤔Mathias wrote on :
MaxArt:
eval(code)
is a “directeval
” and executes code in the current scope.(0, eval)(code)
is an indirecteval
and executes code in the global scope.Sichao Zhang wrote on :
Hi, very informative post that helps me clear a bunch of blind spots! I have a question regarding the following two lines:
I’m not quite sure why you have to attach
__magic__
to a property on__magic__
. And when you do thedelete
operation on__magic__
, doesn’t that delete theglobalThis
property as well?Thank you for your explanation in advance!
Mathias wrote on :
Sichao: The goal of the polyfill is to make
globalThis
globally available. Global variables can be created by adding them as properties to the globalthis
.In the code snippet you highlight,
__magic__
is a reference to the globalthis
, or what you know aswindow
in most browser contexts. So this line:…is equivalent to the following in a browser context where
window
is available:…which makes
globalThis
globally available.The
delete
line removes the getter, since we only need it once to get the reference to the globalthis
.