Why Top-Level const Is Not a window Property
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
constin a classic browser script creates a global lexical binding that bare identifiers can find, but it does not create a property onwindoworglobalThis.
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.
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, andclassbindings. - The object record is backed by the global object and contains built-in globals, top-level
varbindings, 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.
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:
| Declaration | Bare name works | window.name | globalThis.name | Own global property | Reassignment | Same-form redeclaration |
|---|---|---|---|---|---|---|
var | Yes | Yes | Yes | Yes | Yes | Yes |
let | Yes | No | No | No | Yes | No |
const | Yes | No | No | No | No | No |
function | Yes | Yes | Yes | Yes | Yes | Yes |
“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.
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.
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
Choose the sharing mechanism from the code that needs the value:
- Use
exportandimportwhen application modules need to share a value. - Assign
globalThis.myAPIwhen the design deliberately requires a global property across JavaScript environments. - Use
windowwhen 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.