Why Top-Level const Is Not a window Property

Oct 2, 2026·17 min read

A page printed 42 when it read answer, then printed undefined when it read window.answer. Both expressions used the same spelling, but they performed different lookups.

A top-level const in a classic browser script creates a global lexical binding that bare identifiers can find, but it does not create a property on window or globalThis.

The Short Answer

A binding connects a name in JavaScript source code to a value. When JavaScript evaluates the identifier answer, it searches the current scope and then its outer scopes for a binding with that name.

A property is a key stored on an object. When JavaScript evaluates window.answer, it looks for the property named "answer" on the window object.

Those operations can produce different results. This standalone HTML document shows the split:

<!doctype html>
<meta charset="utf-8">
<title>const and window</title>

<script>
  const answer = 42;

  console.log(answer);
  console.log(window.answer);
  console.log(Object.hasOwn(window, "answer"));
</script>
42
undefined
false

The first line finds the answer binding. The second asks window for an answer property, which was never created. The third confirms that window does not own such a property.

In ordinary browser code, window provides access to the page’s global object; technically, browsers expose it through a WindowProxy. That does not mean every global name is stored on it. A classic script has a global environment that can hold both object properties and lexical bindings.

bare nameanswerwindow.answerGlobal environmentLexical sidelet · constclassObject-backedsidevar · functionproperties
Bare names search the whole global environment; window properties use only its object-backed side.

Global scope is not the same thing as the set of properties on window.

This rule applies to top-level let, const, and class declarations in classic scripts. Top-level var and function declarations take the object-backed route instead, so their names normally appear as properties.

Global Scope Is Bigger Than the window Object

JavaScript models the scope used by classic scripts with a Global Environment Record. An environment record is the mechanism JavaScript uses to resolve names. The global version combines two conceptual components:

  • The declarative record holds top-level let, const, and class bindings.
  • The object record is backed by the global object and contains built-in globals, top-level var bindings, top-level function declarations, and explicitly assigned properties.
bare identifier
      |
      v
Global Environment Record
      |-- declarative record: let, const, class
      `-- object record: var, function, global properties

window.name / globalThis.name
      |
      `--------> object record only

These are two parts of one global environment, not two different levels of scope. A name in either component can be global, but only names represented by properties appear during object-property lookup.

Say a classic script declares four names:

<script>
  var edition = 3;
  let theme = "night";
  const answer = 42;

  function buildLabel() {
    return "ready";
  }
</script>

Identifier lookup can find all four names. Property lookup finds edition and buildLabel, because their declarations use the object-backed component. It does not find theme or answer, because those use the declarative component.

This distinction also explains why changing const to let does not make a name appear on window. Both declaration forms use the same declarative side. Their difference is reassignment: a let binding can receive another value, while a const binding cannot.

That restriction applies to the binding, not necessarily to the value. A const can refer to an object whose properties still change:

const settings = { theme: "night" };

settings.theme = "day";

console.log(settings.theme);
day

The settings binding still refers to the same object. The object itself now has a different theme property. Variables and bindings provides the broader rules for declarations, assignment, and scope.

One fixed binding, one mutable objectconstsettingsbinding staysrefers tosame objecttheme propertynightdayproperty can change
const fixes which object the binding refers to, not every property inside that object.

The global object also depends on the environment. Browser pages expose window, workers use a different global object, and other JavaScript environments have their own global values. globalThis is the standard spelling for accessing the global this value across those environments.

var, let, const, and function Compared

The declaration form decides both where the global name lives and whether its binding accepts reassignment. Those are separate questions.

This table compares declarations at the top level of a classic browser script:

DeclarationBare name workswindow.nameglobalThis.nameOwn global propertyReassignmentSame-form redeclaration
varYesYesYesYesYesYes
letYesNoNoNoYesNo
constYesNoNoNoNoNo
functionYesYesYesYesYesYes

“Own global property” means Object.hasOwn(window, name) returns true. It does not describe whether a value is mutable.

Here is the canonical test page. It covers all four declaration forms, access through window and globalThis, a later classic script, and module top level in one file:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Global declaration matrix</title>
</head>
<body>
  <pre id="output"></pre>

  <script>
    function write(line) {
      document.querySelector("#output").textContent += `${line}\n`;
    }

    var archiveCount = 3;
    let theme = "night";
    const answer = 42;

    function buildLabel() {
      return "ready";
    }

    write([
      "declaration",
      "bare",
      "window",
      "globalThis",
      "own property",
    ].join(" | "));

    write([
      "var archiveCount",
      archiveCount,
      window.archiveCount,
      globalThis.archiveCount,
      Object.hasOwn(window, "archiveCount"),
    ].join(" | "));

    write([
      "let theme",
      theme,
      window.theme,
      globalThis.theme,
      Object.hasOwn(window, "theme"),
    ].join(" | "));

    write([
      "const answer",
      answer,
      window.answer,
      globalThis.answer,
      Object.hasOwn(window, "answer"),
    ].join(" | "));

    write([
      "function buildLabel",
      buildLabel(),
      window.buildLabel(),
      globalThis.buildLabel(),
      Object.hasOwn(window, "buildLabel"),
    ].join(" | "));
  </script>

  <script>
    write([
      "later classic",
      answer,
      window.answer,
      Object.hasOwn(window, "answer"),
    ].join(" | "));

    // Uncommenting this declaration makes this second script fail:
    // const answer = 99;
  </script>

  <script type="module">
    var moduleCount = 7;
    const moduleAnswer = 99;

    function moduleLabel() {
      return "ready";
    }

    write([
      "module var",
      moduleCount,
      globalThis.moduleCount,
      Object.hasOwn(globalThis, "moduleCount"),
    ].join(" | "));

    write([
      "module const",
      moduleAnswer,
      globalThis.moduleAnswer,
      Object.hasOwn(globalThis, "moduleAnswer"),
    ].join(" | "));

    write([
      "module function",
      moduleLabel(),
      globalThis.moduleLabel,
      Object.hasOwn(globalThis, "moduleLabel"),
    ].join(" | "));

    write([
      "module top-level this",
      String(this),
    ].join(" | "));
  </script>
</body>
</html>
declaration | bare | window | globalThis | own property
var archiveCount | 3 | 3 | 3 | true
let theme | night | undefined | undefined | false
const answer | 42 | undefined | undefined | false
function buildLabel | ready | ready | ready | true
later classic | 42 | undefined | false
module var | 7 | undefined | false
module const | 99 | undefined | false
module function | ready | undefined | false
module top-level this | undefined

Save that document and open it as a page. The <pre> element shows the matrix without depending on a browser console.

The var and function rows reach the same values through bare names, window, and globalThis. The let and const rows work only as identifiers. Their missing properties produce undefined, and Object.hasOwn reports false.

The old var explains the other scope differences that remain after this global-property rule is understood.

What Happens Across Multiple script Elements

Classic script elements processed in the same page share the page’s global environment. That is why the second classic script in the test page can read answer even though the first script created no window.answer property.

The order matters. The browser runs the first classic script, creates the global lexical binding, and assigns it 42. When the next classic script evaluates the bare identifier answer, normal identifier lookup finds that earlier binding.

One page, scripts processed in ordertime moves forwardfirst scriptconst answer= 42later scriptanswerwindow.answershared lexicalanswer → 42window propertyundefined
Later classic scripts share the lexical binding, but window still has no matching property.

Property lookup still follows the other route:

answer
window.answer
Object.hasOwn(window, "answer")

These expressions produce 42, undefined, and false in the test page. Moving into a later classic script changes none of them.

A later classic script cannot declare another top-level let or const with the same name. If the commented const answer = 99 line in the canonical page is restored, the second script fails before its statements run because answer already exists as a global lexical binding.

The future declaration does not reach backward. A declaration in a later script does not place an earlier script inside its temporal dead zone, which is the period before a lexical binding has been initialized. Scripts are processed in order, and the later declaration is not part of the earlier script’s declaration setup.

This gives classic scripts an unusual combination: the lexical binding is shared across the page, but it is not a property you can inspect on window. The name is global. Its storage is not object-backed.

Closures and lexical scope develops the same identifier lookup model inside functions, where names can also remain available without being object properties.

Modules Change What Top Level Means

A <script type="module"> uses module scope. Top-level declarations in that module belong to the module, whether they use var, let, const, class, or function.

The module part of the canonical page demonstrates the result. moduleCount, moduleAnswer, and moduleLabel all work as bare names inside that module, but none becomes a property of globalThis.

This is different from a classic script’s split global environment. In a classic script, top-level var and function declarations use the object-backed component while top-level lexical declarations use the declarative component. In a module, all of those top-level declarations remain module-scoped.

Classic scriptlet · const · classvar · functionlexicalstoreglobalobjectModulevar · let · constclass · functionmodule scopeonlyglobalThis: noproperty
Classic scripts split declarations by kind; modules keep every top-level declaration inside the module.

Top-level this also changes:

<script>
  console.log(this === globalThis);
</script>

<script type="module">
  console.log(this);
</script>
true
undefined

The classic script’s top-level this refers to the global this value, which is normally window in a browser page. The module’s top-level this is undefined. Module scripts also run in strict mode automatically and are deferred automatically.

Values should cross module boundaries through export and import. Start with a module that exports the value:

// settings.js
export const answer = 42;

Then import that binding where it is needed:

// app.js
import { answer } from "./settings.js";

document.querySelector("#output").textContent = String(answer);

Load the importing file as a module:

<pre id="output"></pre>
<script type="module" src="./app.js"></script>

answer remains a module binding. Neither file needs to create window.answer, and another module can import the same exported binding by name.

The browser environment and its specifications provide the wider setting for classic scripts, modules, window, and the APIs attached to it.

How to Share a Value Intentionally

Two deliberate ways to shareNamed module bridgesettings.jsanswer = 42exportand importapp.jsuses answerExplicit global propertyassign propertycourseSettingsglobalThispropertycode readsthe property
Choose a named module bridge or an explicit global property according to who needs the value.

Choose the sharing mechanism from the code that needs the value:

  • Use export and import when application modules need to share a value.
  • Assign globalThis.myAPI when the design deliberately requires a global property across JavaScript environments.
  • Use window when the code specifically works with browser page APIs.
  • Use Object.hasOwn(globalThis, "name") when checking whether an explicit global property exists.
  • Use a bare identifier when reading a binding that is already in lexical scope.

For a deliberate global property, write the property assignment directly:

globalThis.courseSettings = {
  edition: 3,
  format: "offline",
};

console.log(globalThis.courseSettings.edition);
console.log(Object.hasOwn(globalThis, "courseSettings"));
3
true

This code says what it is doing. It does not depend on classic-script declaration rules, and the property is observable through globalThis.

Avoid creating globals by assigning to an undeclared identifier. That assignment is an error in strict mode and modules, and it hides the design choice in what looks like a missing declaration.

When debugging an unfamiliar name, inspect both lookup paths. First, use the identifier where it is valid. Then check Object.hasOwn(globalThis, "name"). If the identifier works and the property check is false, the name may be a lexical binding rather than a global-object property.

For larger codebases, explicit module boundaries keep dependencies visible. Patterns and Architecture carries that decision into application structure, where shared state and public APIs need named owners rather than accidental globals.

Frequently asked questions

Why is a top-level const not on window?
A top-level const in a classic script creates a global lexical binding, not a property on the browser's global object. JavaScript can resolve the bare identifier while window has no property with that name.
Is a top-level const global in JavaScript?
In a classic browser script, a top-level const is global in scope and later classic scripts in the same page can use it. It is still absent from window and globalThis as a property.
Does top-level let become a window property?
No. Top-level let and const declarations in a classic script belong to the declarative part of the global environment, so neither creates a window property.
Are top-level variables in JavaScript modules global?
No. Top-level var, let, const, class, and function declarations in a module remain scoped to that module. Share module values with export and import, or explicitly assign a property to globalThis when a global API is required.