skip to content

In an ES module you write `var config = {}` and `function init() {}` at the top level. Are `globalThis.config` and `globalThis.init` defined afterwards, and how does that differ from the same two lines in a classic (non-module) script?

level: juniorimportance: must knowfreq 70%

answer

  1. each file gets its own top level
  2. var no longer reaches the global object
  3. sharing needs export and import
  4. only an explicit globalThis assignment leaks

basics

~10 s

No. Top-level declarations in an ES module live in that module's own scope, so globalThis.config and globalThis.init stay undefined. In a classic script the same declarations become properties of the global object.

solid answer

~40 s

In an ES module both names stay private to the module: the module body has its own top-level scope, so `globalThis.config` and `globalThis.init` are `undefined`. In classic script code, top-level `var` and function declarations are created as properties of the global object, which is exactly how the old "every file shares one namespace" style worked. A module can still *read* globals — `globalThis`, `console`, `JSON` all resolve through the outer scope — it just never publishes into that scope implicitly. To share `config` with another file you `export` it and the other file `import`s it; to reach the global object you must say so explicitly, for example `globalThis.config = config`. A practical consequence: two modules can each declare a top-level `config` with no collision at all.

code

javascript · 12 lines
javascript
// utils.mjs — module code
var config = { retries: 3 };
function init() { return config; }

console.log(typeof globalThis.config); // "undefined"
console.log(typeof globalThis.init);   // "undefined"

// the only way to reach the global object is to say so
globalThis.__APP_CONFIG__ = config;
console.log(typeof globalThis.__APP_CONFIG__); // "object"

export { config, init };

go deeper

for a junior

Know that top-level declarations in a module stay inside that file, and that sharing a value across files means writing export in one and import in the other rather than relying on a global.

for a middle

Explain the mechanism: module code creates its own environment record whose outer scope is the global environment, so lookups still fall outward while declarations never fall in. Mention that var gets no special treatment in a module.

for a senior

Be ready to diagnose the symptom in a real codebase — a DevTools console experiment that works while the same line fails in a file, or a legacy file that silently stopped publishing its API — and to define the one explicit globalThis boundary where handoff is allowed.

for a principal

Frame it as a namespace-governance decision: module scope makes a file's public surface declared rather than emergent, which is what makes cross-team code auditable. Own the policy that global assignments live in one boundary file and carry a plan to be retired.

## The answer up front Nothing is created on the global object. `globalThis.config` and `globalThis.init` are both `undefined`. Run the same two lines in a classic script and both become global-object properties, so any other script could read the bare names. ## Two different "top levels" ECMAScript parses source text with one of two goals: **Script** or **Module**. That choice is made by whatever loads the file, and it changes what "top level" means. In **script** code, the top-level environment *is* the global environment. `var` and function declarations there create properties on the global object; `let`, `const` and `class` go into a separate global declarative record — they are not properties of `globalThis`, but they still live in the one global scope every script on the page shares. In **module** code, evaluation creates a fresh *module environment record* for the module body. Its outer environment is the global environment, so lookups still fall through to globals, but every declaration you write — `var`, `let`, `const`, `function`, `class`, and imported names — is created in the module's own record. ```js // classic script var config = {}; // globalThis.config === config function init() {} // globalThis.init === init ``` ```js // ES module var config = {}; // globalThis.config === undefined function init() {} // globalThis.init === undefined export { config, init }; ``` Note that `var` gets no special treatment in a module. The "var attaches to the global object" rule was never about `var` itself; it was about top-level script code being global code. ## Reading versus publishing Isolation is one-directional. A module resolves any name it does not declare by walking outward to the global environment, so `globalThis`, `console`, `JSON`, `setTimeout` and anything a previous script parked on the global object are all readable. What changed is *publishing*: a module contributes nothing to the global namespace unless you assign to `globalThis` (or to a property of it) on purpose. ```js // deliberate, visible handoff — the only way out of module scope globalThis.__APP_VERSION__ = '2.4.1'; ``` That explicitness is the point of the design. In the script era, whether a file exported something was invisible: you had to read the whole file and hope nobody later reassigned the same global. In modules, the surface is declared in the source. ## Why interviewers ask it Because it is the single behaviour that breaks pasted-in legacy code. Three symptoms come up constantly: - **"It works in the DevTools console but not in my file."** The console evaluates what you type as script code, so `var x = 1` there really does create `globalThis.x`. Your module does not. - **"Another file can't see my function."** In the script era, file B just used a name file A declared. In modules there is no shared namespace: file A must `export` it and file B must `import` it. - **Name shadowing surprises.** In a browser, `window.name` already exists as a string. A top-level `const name = 'checkout'` in a module creates a module binding that shadows it locally and leaves the global untouched — clean. The script-era equivalent stomped on a real platform property. ## Scope, instance, and lifetime Each module gets exactly one environment per module record: the body runs once, and the bindings it creates live as long as the module is reachable. So a top-level `let cache = new Map()` in a module behaves like a per-module singleton shared by every importer of that module — not a global, but not per-call either. That is worth saying out loud, because it is the useful half of the isolation: private state that survives across calls without touching the global object. ## How to demonstrate it The crisp check is `typeof globalThis.config`. If the file is running as a module it prints `"undefined"`; if it is running as a script it prints `"object"`. That one line is also the fastest way to settle an argument about whether a given file is actually being evaluated as a module. `globalThis` itself is the ES2020 spelling of "the global object"; older code says `window` in a browser. The module-scope rule described here is unchanged since modules were introduced in ES2015.

  • Does the same isolation apply to `let` and `const` at the top level of a classic script?
    Partly. In a script, `let`, `const` and `class` do not become properties of the global object — they go into the global declarative record. But that record is still shared by every script in the page, so two scripts declaring `let total` collide with a SyntaxError. Modules have no shared record at all, so identical top-level names in two modules are simply unrelated.
  • If two modules each declare a top-level `let count`, do they interfere?
    No. Each module body has its own environment record, so the two `count` bindings are unrelated variables. That is why modules removed the need for filename-prefixed global names. The only way one module observes the other's value is through an explicit export/import relationship.
  • How do you deliberately expose something to non-module code on the page?
    Assign it to the global object yourself: `globalThis.myLib = myLib`. It is an explicit, greppable line rather than an accident of where a `var` happened to sit, and it keeps the handoff visible in code review. Keep such assignments to a single boundary file rather than scattering them.

A script file is like writing on a shared office whiteboard; a module is like writing in your own notebook and photocopying only the pages you choose to hand out.

saying these in an interview costs you the question

  • Thinks a top-level var in a module still creates window.foo
  • Believes the bundler, not the language, provides module isolation
  • Says modules cannot read globals like console or globalThis
  • Assumes two modules with the same top-level name collide
  • Thinks let and const behave differently from var here

context