skip to content

What does top-level `this` evaluate to in a classic browser script, in an ES module, and in a Node CommonJS module, and why is `this` an unreliable way to reach the global object?

level: middleimportance: should knowfreq 45%

answer

  1. three source types, three answers
  2. the same file can change meaning
  3. module scope is deliberately sealed off
  4. CommonJS wraps the body in a function

basics

~20 s

Top-level this is the global object in a classic script, undefined in an ES module, and module.exports in a CommonJS module. Because the value depends on how the file is loaded, use globalThis when you actually want the global object.

solid answer

~40 s

Three source types, three answers. In a classic script — a plain `<script>` — top-level `this` is the global object, so `this === globalThis` is true. In an ES module, the module's `[[ThisValue]]` is specified as `undefined`, so top-level `this` is `undefined` and `this.foo = 1` throws a TypeError. In a Node CommonJS module the file body is wrapped in a function that is called with `module.exports` as its receiver, so top-level `this` is that initially-empty exports object. The practical consequence is that `var root = this;` at the top of a file is not portable — the same source silently means three different things depending on how it is loaded, which is a classic bug when a script is later converted to a module. When you want the global object, name it: `globalThis`.

code

javascript · 11 lines
javascript
// index.cjs  —  run with: node index.cjs
console.log(this === module.exports); // true
console.log(this);                    // {}

this.a = 1;
console.log(module.exports);          // { a: 1 }

module.exports = { b: 2 };            // replaces the object
console.log(this === module.exports); // false — this is now orphaned
this.c = 3;
console.log(module.exports);          // { b: 2 } — c is lost

go deeper

for a junior

Remember that a plain script's top-level this is the global object and that an ES module's is undefined, and reach for globalThis whenever you actually mean the global object.

for a middle

Be ready to name all three source types and their values, and to explain the CommonJS wrapper function that makes top-level this equal to module.exports.

for a senior

Show that you catch this in review: spot files whose source type is ambiguous, and explain how a script-to-module migration turns a silent global write into a runtime TypeError somewhere far away.

for a principal

Own the consistency call — a codebase that fixes one module system, forbids top-level this entirely, and routes deliberate globals through a single named entry point removes this whole class of migration surprise.

## Why there is more than one answer `this` at the top level of a file is not a single rule; it is decided by the *kind* of code being evaluated. JavaScript has several source types and each one specifies its own top-level `this` value. ## Classic script A file loaded with a plain `<script src="...">` (no `type="module"`) is evaluated as a **script**. Script evaluation runs in the global execution context, whose `this` value is the global object. ```js // classic script console.log(this === globalThis); // true this.appName = 'demo'; console.log(globalThis.appName); // 'demo' ``` This is why the old `var root = this;` idiom worked at all — it was written in an era when nearly all browser code was classic scripts. ## ES module A file loaded with `<script type="module">`, an `.mjs` file, or any file in a package whose `type` is `"module"`, is evaluated as a **module**. The specification sets a module's this-value to `undefined`, so: ```js // ES module console.log(this); // undefined this.appName = 'demo'; // TypeError: Cannot set properties of undefined ``` That is deliberate. Modules are meant to be self-contained: nothing about them should encourage writing to the global object by accident. If you genuinely want a global, you have to say so: `globalThis.appName = 'demo'`. This is also the single most common way the old idiom breaks. A file that worked for years as a script is switched to a module during a build migration, `var root = this;` quietly becomes `undefined`, and the failure surfaces much later as `Cannot read properties of undefined` somewhere unrelated. ## Node CommonJS module A `.cjs` file, or a `.js` file in a package without `"type": "module"`, is loaded by Node's CommonJS loader. That loader wraps the file body in a function and invokes it with `module.exports` as the receiver. So: ```js // index.cjs console.log(this === module.exports); // true console.log(this); // {} (empty until you add exports) this.hello = () => 'hi'; // a real, if unusual, way to export ``` Note the trap in the last line: `this.hello = ...` mutates the exports object, but `module.exports = something` **replaces** it, after which top-level `this` no longer refers to what the module exports. ## What about `this` inside functions? Top-level `this` is a separate question from `this` inside a function, which is decided at call time by how the function is called. The two interact in one place worth knowing: an ordinary function invoked with no receiver gets `undefined` as `this` in strict-mode code and the global object in non-strict code. ES modules are always strict, so inside a module a bare call never gives you the global object either. That is the second reason the `Function('return this')()` trick had to build its function from a string — a function created that way is not strict. An arrow function at the top level inherits the surrounding `this`, so an arrow declared at module top level closes over `undefined`, not over the global object. ## The takeaway Do not use `this` to mean "the global object". It means that in exactly one of the three source types, and the same file can move between source types without any change to its contents. If you want the global object, write `globalThis` — one name, one meaning, every host. If you want the module's exports in CommonJS, write `module.exports` explicitly rather than relying on the wrapper's receiver. ## How to test yourself in a review When you see top-level `this` in a diff, ask which source type the file is: does it have `import`/`export` statements, what is the `type` field of the nearest `package.json`, is it loaded with `type="module"`? If the answer is not obvious from the file alone, that alone is a reason to replace `this` with an explicit name.

  • A file that worked as a classic script starts throwing 'Cannot set properties of undefined' after being converted to an ES module. What is the likely cause?
    Top-level `this` was the global object under script evaluation and is specified as `undefined` in a module, so a line like `this.myGlobal = x` now throws. The fix is to name what you actually meant: `globalThis.myGlobal = x` for a real global, or better, export the value and let importers bind it.
  • In a CommonJS module, why can `this` stop matching the module's exports partway through the file?
    Because the loader calls the wrapper with the *original* `module.exports` object as receiver. Mutating it (`this.foo = 1`) keeps them in sync, but reassigning (`module.exports = something`) rebinds only the `module.exports` property — `this` still points at the now-orphaned original object, so later `this.bar = 2` writes to something no importer will ever see.
  • What does an arrow function declared at the top level of an ES module see as its `this`?
    `undefined`. Arrow functions have no `this` of their own and capture the enclosing lexical `this`, which at module top level is `undefined`. So an arrow passed elsewhere as a callback cannot be given a receiver later either — call, apply and bind cannot change an arrow's this.

saying these in an interview costs you the question

  • Says top-level this is always the global object
  • Assumes an ES module and a classic script behave identically
  • Thinks module top-level this is an empty object like CommonJS
  • Claims this in a module can be fixed by calling it differently
  • Uses var root = this as a portable global reference

context