Back

A Quick Guide to JavaScript Global Scope

A Quick Guide to JavaScript Global Scope

When you declare a variable at the top level of a JavaScript file, where does it actually live? The answer depends on whether you’re writing a classic script or an ES module—and getting this wrong leads to subtle bugs that are frustrating to debug.

This guide explains how JavaScript global scope works in modern development, why globalThis is the reliable way to reference the global object, and how var, let, and const behave differently at the top level.

Key Takeaways

  • Global scope behavior differs between classic scripts and ES modules—only var in classic scripts creates properties on the global object.
  • let and const create global bindings but don’t attach to the global object.
  • Use globalThis for cross-environment compatibility when you need to access the global object.
  • Prefer ES modules for automatic scope isolation and to avoid unintended namespace pollution.

What Is JavaScript Global Scope?

Global scope refers to the outermost scope in your JavaScript environment. Variables in global scope are accessible from anywhere in your code—inside functions, blocks, or nested modules.

But here’s the critical distinction most tutorials miss: top-level scope in JavaScript behaves differently depending on how your script loads.

Classic Scripts vs ES Modules

In a classic script (loaded without type="module"), top-level variables declared with var become properties of the global object:

// classic script
var config = { debug: true }
console.log(globalThis.config) // { debug: true }

With ES modules, this changes completely. Modules and global scope work differently by design. Top-level bindings in a module stay within that module’s scope—they don’t attach to the global object:

// module script (type="module")
var config = { debug: true }
console.log(globalThis.config) // undefined

This isolation is intentional. Modules provide encapsulation, preventing accidental namespace pollution across files. See the MDN guide on JavaScript modules for a deeper reference.

How var, let, and const Differ at the Top Level

Even in classic scripts, let and const behave differently from var:

// classic script
var a = 1
let b = 2
const c = 3

console.log(globalThis.a) // 1
console.log(globalThis.b) // undefined
console.log(globalThis.c) // undefined

Only var creates a property on the global object. Both let and const create bindings in the global scope that are accessible throughout your code, but they don’t become properties of the global object.

This distinction matters when third-party scripts expect to find your variables on the global object, or when you’re debugging and checking it directly.

Why globalThis Is the Modern Standard

Different JavaScript environments historically used different names for the global object:

  • Browsers use window
  • Node.js uses global
  • Web Workers use self

Writing cross-environment code meant checking which global existed. The globalThis standard solves this by providing a universal reference:

// Works everywhere
globalThis.sharedValue = 'accessible across environments'

Using globalThis ensures your code runs correctly whether it executes in a browser, Node.js, Deno, or a Web Worker. It’s the correct, platform-agnostic approach.

Variable Shadowing in Global Scope

A common misconception: you cannot redeclare a let or const variable that already exists in global scope. However, you can shadow globally-introduced var bindings with lexical declarations—including var bindings introduced dynamically (for example, via eval) in modern engines:

var x = 1
{
  let x = 2 // shadows the global var
  console.log(x) // 2
}
console.log(x) // 1

The inner let x creates a separate variable that temporarily hides the outer var x within that block. They’re distinct bindings—modifying one doesn’t affect the other.

Module-Only Features: Top-Level Await

One practical difference with top-level scope that JavaScript developers should know: await at the top level only works in modules:

// Only valid in modules
const data = await fetch('/api/config').then(r => r.json())

Classic scripts don’t support this syntax. If you need top-level await, you must use type="module" on your script tag.

Best Practices for Global Scope

  1. Prefer modules — They provide automatic scope isolation and explicit imports/exports
  2. Use globalThis — When you genuinely need the global object, this works everywhere
  3. Avoid var at the top level — Use let or const to prevent unintended global object pollution
  4. Be explicit about globals — If something must be global, attach it to globalThis intentionally rather than relying on implicit behavior

Conclusion

Global scope in JavaScript isn’t as simple as “variables accessible everywhere.” Classic scripts and ES modules handle top-level bindings differently, and only var in classic scripts creates global object properties. Use globalThis for cross-environment compatibility, and prefer modules for their built-in encapsulation. Understanding these distinctions helps you write predictable code that works across different JavaScript environments.

FAQs

Yes, but only if both scripts are classic scripts and share the same global scope. The variables are accessible by name across scripts, but they are not properties of the global object. If either script is an ES module, the variables remain private to that module unless explicitly exported.

Use the global object when you need to share data with scripts you don't control, such as third-party analytics or legacy code that expects globals. Also use it for polyfills that must be universally available. For code you control, prefer module exports for better encapsulation and explicit dependency tracking.

globalThis is supported in all modern JavaScript environments. If you must target legacy platforms such as Internet Explorer, use a polyfill or rely on your bundler/transpiler to provide one.

This is a historical design decision. var was part of JavaScript from the beginning and was designed to create global object properties for interoperability. let and const were introduced in ES6 with stricter scoping rules to avoid the pitfalls of implicit global pollution while still allowing global accessibility.