The old var & the global object
Understand how var and globals behave in classic scripts, why legacy code can leak names onto window, and how modules, let, const, and strict mode prevent global pollution.
- 01Function scopeWhy
varignores blocks and can be redeclared. - 02Two kinds of globalWhich top-level names become
windowproperties and which stay lexical. - 03Global pollutionHow implicit globals and script collisions happen, and how to avoid them.
Why old globals still matter
Modern JavaScript usually lives in modules, uses let and const, and avoids the global object. But a lot of code you will read is older: analytics tags, browser snippets, old libraries, and pre-bundler applications. That code often uses var, expects names to appear on window, and sometimes creates globals by accident.
This lesson connects three ideas you have already met: the Variables lesson warned you to avoid var, the Execution contexts lesson introduced the global execution context, and the Strict mode lesson showed how modern JavaScript catches silent mistakes. Now we go deeper so legacy code makes sense.
In a browser classic script, top-level var and top-level function declarations become properties of the global object: window. Top-level let and const are still global to scripts, but they are not window properties. Modules are different again: their top level belongs to the module.
A var declared inside an if block is like a notice pinned to the whole building’s lobby board instead of your room’s door. Anyone on that floor of the building—the whole function—can see it, even if the notice was written while standing inside one room.
- In real life: A notice pinned to the building lobby
- In JavaScript: A
varbinding visible across the whole function - In real life: A note taped to your room door
- In JavaScript: A
letorconstbinding visible only in its block - In real life: A public city square notice
- In JavaScript: A
windowproperty in a classic browser script
Where the analogy stops: Scope is not a physical place, and JavaScript decides visibility from the source code structure, not from where a value is stored in memory. The analogy is about who can see the name.
Keep one honesty rule in mind: this site’s lesson files are ES modules. If we wrote top-level var directly in this component, it would not create a window property, and a typo assignment would throw. The classic browser behavior below runs for real inside a sandboxed iframe, and the tests also prove classic-script behavior with Node’s vm classic scripts.
Function-scoped var
STEP THROUGHvar has function scope. A var declared anywhere inside a function belongs to that entire function. Blocks made by if, for, or plain braces do not create a new scope for var. let and const do use block scope.
Switch line 3 between var and let. The code is tiny, but it explains a huge number of legacy bugs.
Switch var to let, then step through why one name leaks out of the block and the other does not.
script
if (show) { var notice = "Lobby board"; } console.log(notice);}readNotice(true);With var, line 5 can read notice after the if block. With let, line 5 is outside the binding’s block and throws a ReferenceError. The upcoming Hoisting & the temporal dead zone lesson explains the creation phase details; for now, remember the practical rule: var belongs to the nearest function, or to the global script if there is no function.
function readNotice(show) { if (show) { var notice = "Lobby board"; } console.log(notice);}readNotice(true);Redeclaration: var shrugs, let objects
COMPAREIn the same scope, var can be declared again. It does not create a fresh variable; it reuses the same binding. That made old scripts forgiving, but it also made collisions easy to miss.
Redeclaring var is like pinning a second notice with the same title to the public board. var shrugs. let and const object because two block-scoped bindings with the same name in the same scope would make the code ambiguous.
- In real life: Pinning a second notice titled Schedule
- In JavaScript:
var schedule; var schedule;is allowed - In real life: The office manager rejects a duplicate private form
- In JavaScript:
let schedule; let schedule;is a SyntaxError - In real life: The newer notice may cover the old wording
- In JavaScript: A later assignment overwrites the same binding
Where the analogy stops: A redeclaration is a syntax rule, not a document filing system. The point is that var tolerates duplicates in one scope, while lexical declarations protect you from them.
var status = "draft";var status = "published"; // allowed: same binding let mode = "dark";let mode = "light"; // SyntaxError before this script can runA SyntaxError is different from a runtime error: JavaScript rejects that script before executing it. In the iframe lab, the bad let redeclaration is placed in its own classic script tag so the page can still report the error afterward.
globalThis & window
REAL IFRAMEThe global object is the object a JavaScript host provides for globally available properties. In a browser page, that object is exposed through window, and globalThis === window is true in ordinary page scripts. The browser also has aliases such as self and frames. In workers, there is no window, but globalThis still points at the worker’s global object. In Node.js, globalThis points at Node’s global object.
globalThis was standardized so code could say “the global object” without guessing whether the host calls it window, self, or something else.
<script>var classicCount = 1;let lexicalCount = 2;function classicTool() {}implicitName = "Ada";var name = 123;</script>The sandbox will post real classic-script results after it runs.
The iframe is a real browser classic-script environment. The page code validates event.source before trusting the posted results. This site's ES modules would not show these classic-script leaks.
allow-scripts; no user code is evaluated. The let redeclaration is intentionally isolated in its own script tag because a syntax error stops that script before it can run.window.name is special in browsers. It stores a string. In a classic script, var name = 123; interacts with that property, so typeof name is "string" and the value behaves like "123". Avoid global names like name; they may already mean something to the host.
The global object vs global lexical bindings
SORTER“Global” has two layers in classic browser scripts. The global object record handles names that are properties of window. The global lexical environment handles top-level let, const, and class. Both are global for script name lookup, but only one is posted in the public square as window.someName.
| Kind | Created by | Visible as property? | Best use |
|---|---|---|---|
| Global object property | Classic-script var, classic-script function declarations, explicit globalThis.x = ..., and sloppy implicit globals | Yes: window.x in browsers | Rare legacy interop. Prefer a single namespace if old scripts need one shared hook. |
| Global lexical binding | Top-level let, const, and class in a classic script | No: the name is global to scripts, but not window.x | Small demos or old pages that cannot use modules yet. |
| Module binding | Top-level declarations in an ES module | No: module top level is file-scoped | Modern application code. Export only what other modules need. |
Sort each declaration by whether it becomes a window property. Pay attention to classic scripts versus modules.
var count = 1;function start() {}let count = 1;const API = {};var count = 1; inside type=modulefunction start() {} inside type=module
Classify each top-level declaration. Assume a browser unless the card says it is inside a module.
Implicit globals: graffiti on the public square
In sloppy classic scripts, assigning to a name that was never declared creates a property on the global object. That is an implicit global. It often starts as a typo.
function saveUser() { let name = "Ada"; nmae = name; // typo: creates window.nmae in sloppy classic scripts}That typo is graffiti: a misspelled assignment quietly paints a new name on the public square. In strict mode, the same line throws a ReferenceError. ES modules are always strict, which is one reason they are safer by default.
If old code depends on implicit globals, adding "use strict" may reveal errors. That is good: declare the variable explicitly, or attach one intentional property such as globalThis.App if a legacy script truly needs a shared namespace.
Global pollution
INTERACTIVEGlobal pollution means putting too many names into shared global space. The danger is not just messiness. It is collisions between scripts, tests that pass only in one order, and app code that accidentally overwrites a library.
// library-a.jsvar config = { theme: "dark" };function $(selector) { return "A:" + selector; } // app.js, loaded latervar config = { theme: "light" };function $(selector) { return "B:" + selector; }In classic scripts, both files write config and $ to the same global object. The later script wins.
// library-a.jsvar config = { theme: "dark" };function $(selector) { return "A:" + selector; } // app.js, loaded latervar config = { theme: "light" };function $(selector) { return "B:" + selector; } console.log(config.theme);console.log($("button"));Fixes, from strongest to weakest: use modules; wrap old code in an IIFE; keep one deliberate namespace such as globalThis.CompleteFrontend; and declare every name with let, const, or var. The Closure patterns lesson mentions IIFEs, and the future Why modules? lesson returns to this problem from a module-system angle.
“Global means window property.”
Not always. Top-level let and const in classic scripts are global lexical bindings, not window properties.
“Modules and scripts have the same top level.”
No. Modules are strict and scoped to the module file. Classic scripts share a global script environment.
“Redeclaring var creates two variables.”
No. It reuses the same binding in the same scope. A later assignment changes that binding.
“Implicit globals are convenient shortcuts.”
They are usually typos or hidden dependencies. Strict mode and modules reject them.
“Only browsers have global objects.”
Every JavaScript host has one. globalThis is the cross-host way to refer to it.
Practice: legacy globals
5 EXERCISESIn a browser classic script, predict the three lines printed by this code.
var score = 10;
let secret = 20;
function report() {}
console.log(globalThis.score);
console.log(globalThis.secret);
console.log(typeof globalThis.report);The output is 10, undefined, and function. score and report are properties on globalThis; secret is a global lexical binding, so globalThis.secret is undefined.
Add strict mode and fix the typo so the function keeps the name local instead of painting a new global.
function saveUser() {
nmae = "Ada";
return nmae;
}
console.log(saveUser());"use strict";
function saveUser() {
let name = "Ada";
return name;
}
console.log(saveUser());Strict mode catches the undeclared assignment. The fixed version declares name locally and returns it, so the console prints Ada without creating globalThis.nmae.
Rewrite the function with let so the loop helper names do not leak across the whole function.
function totalVisible(items) {
var total = 0;
for (var i = 0; i < items.length; i = i + 1) {
var item = items[i];
if (item.visible) {
total = total + item.price;
}
}
return total;
}function totalVisible(items) {
let total = 0;
for (let i = 0; i < items.length; i = i + 1) {
let item = items[i];
if (item.visible) {
total = total + item.price;
}
}
return total;
}
console.log(totalVisible([{ visible: true, price: 5 }, { visible: false, price: 99 }]));Use let total for the accumulator, let i for the loop counter, and let item inside the loop. The test data prints 5, and item no longer leaks through the whole function.
In the iframe lab, var name = 123; leads to typeof name being what?
The type is string. Browser window.name is a special string-valued property, so var name = 123 in a classic script does not behave like a normal number variable. Avoid global name.
Take the two-script collision from the pollution demo and describe how you would refactor it to modules. What should be exported? What should stay private?
export const config = { theme: "dark" };
export function dollar(selector) { return "A:" + selector; }
import { config, dollar as libraryDollar } from "./library-a.js";Modules give each file its own top-level scope. Names collide only when you import them into the same scope, and you can rename imports to make that explicit.
Quiz: check your understanding
7 QUESTIONSAnswer once from memory, then read every explanation. The wrong choices describe real misconceptions in legacy code.
Question 1 of 7What is the scope of
varinside anifblock in a function?Choose an answer to see the explanation.
Question 2 of 7What does this print?
Read the code, then predictfunction demo() { if (true) { var label = "inside"; } console.log(label); } demo();Choose an answer to see the explanation.
Question 3 of 7In a browser classic script, which top-level declaration becomes a
windowproperty?Choose an answer to see the explanation.
Question 4 of 7What does this classic-script pair print?
Read the code, then predictvar count = 1; var count = 2; console.log(count);Choose an answer to see the explanation.
Question 5 of 7What is an implicit global?
Choose an answer to see the explanation.
Question 6 of 7What does strict mode do to a typo assignment like
nmae = "Ada"?Choose an answer to see the explanation.
Question 7 of 7Why is global pollution dangerous?
Choose an answer to see the explanation.
Key takeaways
varis function-scoped, not block-scoped.varredeclaration is allowed;letandconstreject duplicate declarations in the same scope.- In browser classic scripts, top-level
varand functions becomewindowproperties; top-levelletandconstdo not. - Sloppy implicit globals are accidental global object properties. Strict mode and modules stop them.
- Global pollution causes collisions. Prefer modules, IIFEs, namespaces, and explicit declarations.
Remember the one-liner.
The global object is the public square; modern code keeps most names off the square unless sharing is intentional.
Up next: The this keyword.