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 a
varbelongs to the whole function, not its block, and why it can be declared twice. - 02Two kinds of globalWhich top-level names become
windowproperties, and which stay offwindow. - 03Global pollutionHow typos and clashing scripts fill the global space, and how to avoid it.
Why old globals still matter
Modern JavaScript code usually lives in modules. A module is a file loaded with import, or with <script type="module">. Modern code also makes its variables with let and const. But a lot of code you will read is older. You'll find it in analytics tags, copied browser snippets, and old libraries. That older code often does three things:
- It makes variables with
var. - It expects its names to show up on
window. - It sometimes makes global names by accident.
This lesson builds on three earlier lessons:
- The Variables lesson told you to avoid
var. - The Execution contexts lesson showed the global execution context: the setup JavaScript makes before the first line of a script runs.
- The Strict mode lesson showed how modern JavaScript turns silent mistakes into errors.
Now you'll see what var really does, so old code makes sense when you meet it.
A classic script is a normal <script> tag, without type="module". "Top-level" means not inside any function or block. In a classic script in the browser:
- A top-level
varor function becomes a property of the global object,window. - A top-level
letorconstis still global, but it does not become awindowproperty.
Modules are different again. Their top-level names stay inside the module file.
Picture an office building. The building is a function. Each room is a block, like the braces of an if. Writing a var inside an if block is like pinning your notice to the lobby board, even though you wrote it in one room. Everyone in the building can read it. A let is like a note taped to your room's door: only people in that room see it. A notice in the town square is for the whole city.
- In real life: A notice pinned to the lobby board
- In JavaScript: A
varvariable, seen by the whole function - In real life: A note taped to one room's door
- In JavaScript: A
letorconstvariable, seen only inside its block - In real life: A notice in the town square
- In JavaScript: A
windowproperty in a classic browser script
One note about this page. The lesson files on this site are modules. So if this page itself used a top-level var, it would not create a window property, and a typo would throw an error. To show the old behavior for real, the lab further down runs classic scripts inside a sandboxed iframe: a small, locked-down page inside this page. The tests for this lesson also check the classic-script behavior with Node's vm module, which can run code as a classic script.
var belongs to the whole function
STEP THROUGHvar has function scope. That means a var written anywhere inside a function belongs to the whole function. A block is code inside braces { }, such as the body of an if or a for. A block does not make a new scope for var. let and const are different. They have block scope: they belong only to their block.
function showCart() { if (true) { var item = "tea"; } console.log(item);}showCart();Line 3 sets item inside the if block. Line 5 is after the block, but it can still read item. That's because var puts item in the whole showCart function. The console prints tea. If line 3 used let item instead, item would exist only inside the braces, and line 5 would throw a ReferenceError.
if braces in both versions. With var, the box belongs to the whole showCart function, so line 5 finds it. With let, the box is gone at the closing brace, so line 5 throws a ReferenceError.Now try it yourself. The next example has the same shape. Switch line 3 between var and let, then step through the code one line at a time. This small difference explains many bugs in old code.
Switch between var and let. Then step through to see why one name is still there after the block and the other is 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 block where notice lives, so it throws a ReferenceError.
The step-through also shows that a var name exists from the start of the function, set to undefined, before line 3 gives it a value. The Hoisting & the temporal dead zone lesson, coming up later, explains why. For now, remember one rule: var belongs to the nearest function around it. If there is no function, it belongs to the whole script.
function readNotice(show) { if (show) { var notice = "Lobby board"; } console.log(notice);}readNotice(true);Declaring the same name twice
COMPARETo declare a variable means to create it with var, let, or const. To redeclare it means to declare the same name again in the same scope. var allows this. The second var does not make a new variable. It reuses the same one. That made old scripts forgiving. But it also made it easy to reuse a name by mistake and never notice.
Redeclaring with var is like pinning a second notice called "Schedule" to the lobby board. Nobody stops you, and the new notice covers the old one. With let or const, the office manager refuses the second one.
- In real life: Pinning a second notice called Schedule
- In JavaScript:
var schedule; var schedule;is allowed - In real life: The office manager refuses a second form with the same name
- In JavaScript:
let schedule; let schedule;is aSyntaxError - In real life: The newer notice covers the old one
- In JavaScript: A later assignment replaces the value in the same variable
let and const refuse a second declaration of the same name in the same scope. Two variables with the same name in one place would make it unclear which one you mean.
var status = "draft";var status = "published";console.log(status);Lines 1 and 2 declare the same variable, status. Line 2 also gives it a new value. So line 3 prints published. There is only one status, not two.
var status = "draft";var status = "published"; // allowed: same binding let mode = "dark";let mode = "light"; // SyntaxError before this script can runA SyntaxError is not like other errors. JavaScript reads the whole script before it runs it. If it finds a grammar mistake, like a repeated let, it rejects the whole script and runs no line of it. Other errors, like ReferenceError, happen while the code is running.
In the iframe lab in the next section, the bad let lines sit in their own <script> tag. That way, the other scripts still run and can report the error.
globalThis & window
REAL IFRAMEThe global object is an object whose properties every script can use without importing anything. The host makes it. The host is the program that runs your JavaScript, like a browser or Node.js. Each host has its own global object:
- In a browser page, the global object is
window, soglobalThis === windowistrue. The browser also gives it other names, such asselfandframes. - In a web worker (a script that runs in the background, apart from the page), there is no
window.globalThispoints at the worker's global object. - In Node.js,
globalThispoints at Node's global object.
globalThis was added to JavaScript so code can say "the global object" in every host. You don't need to guess whether the host calls it window, self, or something else.
The lab below runs a few classic scripts inside a sandboxed iframe. Next to the code, it lists what each script created. Click "Run the sandbox again" to run them once more.
<script>var classicCount = 1;let lexicalCount = 2;function classicTool() {}implicitName = "Ada";var name = 123;</script>The sandbox sends its real classic-script results here after it runs.
The iframe is a real browser page that runs classic scripts. Before this page trusts the results, it checks event.source, which says which frame sent the message. This site's own files are modules, so they could not show these classic-script effects.
allow-scripts and runs only the fixed code shown. The repeated let sits in its own script tag, because a SyntaxError stops its whole script before any line runs.In browsers, window.name is a special property. It always stores a string. In a classic script, var name = 123; uses that same property. So the number is turned into the text "123", and typeof name is "string". Avoid global names like name: the browser already uses them.
A browser page starts with many names already on window. For example, window.status already exists and holds a string, even though modern pages rarely use it. Before you add a global name, check that the browser or another script doesn't already use it.
Two kinds of global name
SORTERIn a classic browser script, "global" can mean two different places:
- The global object,
window. Top-levelvarand function declarations go here, as properties. - A separate list of global names for top-level
let,const, andclass. The specification (the official rule book for JavaScript) calls this list the global lexical environment. "Lexical" means "decided by where the code is written".
Every classic script on the page can use names from both places. But only names in the first place show up as window.someName.
var cart = "tea";let checkoutNote = "private";function checkout() {}console.log(globalThis.cart);console.log(globalThis.checkoutNote);console.log(typeof globalThis.checkout);Line 4 finds cart as a property of globalThis and prints tea. Line 5 finds no property called checkoutNote, because a top-level let is not stored on the global object. It prints undefined. Line 6 finds checkout as a property. typeof gives the type of a value as text, so line 6 prints function.
var cart and function checkout become properties of window, the global object. The top-level let checkoutNote is still a global name, but it sits outside window. That is why line 5 prints undefined.The table below lists the three places a top-level name can live. Read each row as: "this kind of name is made by this code, and here is whether you can see it on window."
| Where the name lives | What code makes it | Is it a window property? | When to use it |
|---|---|---|---|
| The global object | A top-level var or function in a classic script, an assignment like globalThis.x = ..., or a sloppy-mode typo. | Yes. In a browser you can read it as window.x. | Rarely, to work with old scripts. If they need a shared entry point, use one namespace object. |
The list of global let, const, and class names | A top-level let, const, or class in a classic script. | No. Every script can use the name, but window.x is undefined. | Small demos, or old pages that can't use modules yet. |
| A module | Any top-level declaration in a module file. | No. The name belongs only to that file. | Modern app code. Export only what other modules need. |
Now sort each declaration by one question: does it become a window property? Check whether the card says it is inside a module.
var count = 1;function start() {}let count = 1;const API = {};var count = 1; inside type=modulefunction start() {} inside type=module
Sort each top-level declaration. Assume a classic browser script, unless the card says it is inside a module.
Implicit globals: a typo that makes a global
Code that is not in strict mode is often called sloppy mode. In a sloppy classic script, you can assign to a name that was never declared, and JavaScript doesn't complain. It adds that name to the global object as a new property. This is called an implicit global: a global made without any declaration. It usually starts as a typo.
function saveUser() { let name = "Ada"; nmae = name; // typo: creates a global in sloppy classic scripts}saveUser();console.log(globalThis.nmae);Line 2 declares name. Line 3 misspells it as nmae. No nmae was ever declared, so in sloppy mode line 3 creates globalThis.nmae. Line 5 runs the function, and line 6 reads the new property. The console prints Ada.
nmae was never declared with let, const, or var. In a non-strict classic script, line 3 quietly adds nmae to window, so line 6 prints Ada. In strict mode, line 3 throws a ReferenceError instead.In strict mode, line 3 throws a ReferenceError instead. Modules always run in strict mode. That is one reason modules are safer.
"use strict";function saveUser() { nmae = "Ada";}saveUser();Here line 1 turns on strict mode. Line 5 calls saveUser, which runs line 3. In strict mode, an unknown name never turns into a global. So line 3 throws a ReferenceError before the function finishes. You see the typo the first time you test the code.
If old code relies on implicit globals, adding "use strict" may show errors. That is good news: each error points at a name to fix. Declare the variable properly. Or, if an old script really needs to share something, add one property on purpose, such as globalThis.App.
Global pollution
INTERACTIVEGlobal pollution means putting too many names into the shared global space. It isn't just messy. It causes real bugs:
- Two scripts pick the same name, and one replaces the other. This is called a collision.
- Tests pass only when the scripts load in one certain order.
- Your app code replaces a library's function by accident.
In the demo below, two scripts both create config and $. Use the button to switch between classic scripts and modules, and watch what the app sees.
// 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 $ onto the same global object. The script that loads later 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"));Lines 2 and 3 come from the library. Lines 6 and 7 come from the app, which loads later and uses the same names. Line 9 prints light, and line 10 prints B:button. The app's versions replaced the library's.
config, and both write it into the same window. The script that loads second replaces the first one's value, so the page sees "light". The same happens to $.Here are ways to fix it, from strongest to weakest:
- Use modules. Each file keeps its own names and shares only what it exports.
- Wrap old code in an IIFE (an immediately invoked function expression). That is a function that runs right away, like
(function () { ... })();. Names made inside it stay inside it. - Keep one shared object on purpose, such as
globalThis.CompleteFrontend, and put the shared names inside it. This one object is called a namespace. - Declare every name with
let,const, orvar, so a typo can't make a new global.
The Closure patterns lesson shows IIFEs. The Why modules? lesson, coming later, returns to this problem.
"Global means window property."
Not always. In a classic script, a top-level let or const is global, but it is not a window property.
"Modules and scripts have the same top level."
No. A module runs in strict mode, and its top-level names belong only to that file. Classic scripts all share one global space.
"Redeclaring var creates two variables."
No. The second var reuses the same variable. A later assignment changes its value.
"Implicit globals are handy shortcuts."
They are usually typos, or hidden links between scripts. Strict mode and modules turn them into errors.
"Only browsers have a global object."
Every host has one: browsers, workers, and Node.js. globalThis is the name that works in all of them.
Sharing one global on purpose
Most app code should live in modules. But sometimes an old page or a plugin needs one public entry point. Then make it easy to see:
- Create one object on
globalThis. - Put a small set of functions inside it.
- Keep every helper name private.
globalThis.CompleteFrontend = { version: "1.0", saveUser(name) { return "saved " + name; },}; console.log(globalThis.CompleteFrontend.saveUser("Asha"));Line 1 creates the one shared object, CompleteFrontend. Line 3 gives it a saveUser method, the only thing this example shares. Line 6 calls it through the object, so the output is saved Asha. One object means fewer collisions. But every script can still change it, so modules are still the better choice.
The delete operator removes a property from an object. It gives true if the property is gone, and false if it could not be removed.
var oldHook = "legacy";globalThis.newHook = "intentional"; console.log(delete globalThis.oldHook);console.log(delete globalThis.newHook);console.log(globalThis.oldHook);console.log(globalThis.newHook);Line 4 prints false. A top-level var makes a property that is non-configurable, which means it cannot be deleted. Line 5 prints true, because a property you add with = can be deleted. Lines 6 and 7 print legacy and undefined: oldHook is still there, and newHook is gone. Don't use delete to clean up a global you did not create.
// module.mjsvar moduleCount = 1;let moduleMessage = "private";console.log(globalThis.moduleCount);console.log(globalThis.moduleMessage);This example only works as a module file, so it has no Run button: the code boxes on this page run code as classic scripts. In a real .mjs file (a file Node.js loads as a module), lines 4 and 5 both print undefined. The module can use its own names, but it doesn't put them on globalThis.
If a script must share a value, write down which script owns it and what the property is called. If it must remove that value later, delete only a property your own code added with =. Then cleanup works the same way every time, even with many scripts on one page.
Practice: old globals
5 EXERCISESThis code runs as a classic script in a browser. Predict the three lines it prints.
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 of globalThis. secret is a top-level let, which is not stored on the global object, so globalThis.secret is undefined.
Add strict mode and fix the typo. The function should keep the name in its own local variable instead of making 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 turns the assignment to an undeclared name into an error. The fixed version declares name inside the function and returns it. The console prints Ada, and no globalThis.nmae is created.
Rewrite the function with let, so the loop's helper names don't spread 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 running total, let i for the loop counter, and let item inside the loop. With the test data, the function returns 5, and item now exists only inside the loop.
In the iframe lab, a classic script runs var name = 123;. What does typeof name give?
The type is string. In browsers, window.name always holds a string. So var name = 123 in a classic script doesn't keep a number. Avoid using name as a global.
Take the two scripts from the pollution demo. Describe how you would turn them into modules. What should each file export? 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";Each module has its own top level, so two files can both have a config without a collision. Names can clash only when you import them into the same file. Then you can rename one import, like dollar as libraryDollar.
Quiz: check your understanding
8 QUESTIONSAnswer each question from memory first. Then read every explanation. The wrong choices are real mistakes people make with old code.
Question 1 of 8A function has a
varinside anifblock. Where can the code use thatvar?Choose an answer to see the explanation.
Question 2 of 8What 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 8In a browser classic script, which top-level declaration becomes a
windowproperty?Choose an answer to see the explanation.
Question 4 of 8What does this classic script print?
Read the code, then predictvar count = 1; var count = 2; console.log(count);Choose an answer to see the explanation.
Question 5 of 8What is an implicit global?
Choose an answer to see the explanation.
Question 6 of 8What does strict mode do to a typo assignment like
nmae = "Ada"?Choose an answer to see the explanation.
Question 7 of 8Why is global pollution dangerous?
Choose an answer to see the explanation.
Question 8 of 8What happens here in a classic script?
Read the code, then predictObject.defineProperty(globalThis, "oldHook", { value: "legacy", configurable: false }); globalThis.newHook = "intentional"; console.log(delete globalThis.oldHook, delete globalThis.newHook);Choose an answer to see the explanation.
Key takeaways
varbelongs to the whole function, not to the block it is written in.- You can declare the same
vartwice, and it is still one variable.letandconstrefuse a second declaration in the same scope. - In a classic browser script, a top-level
varor function becomes awindowproperty. A top-levelletorconstdoes not. - In sloppy mode, assigning to a name that was never declared makes an implicit global, usually by mistake. Strict mode and modules stop this.
- Too many globals cause collisions. Use modules first. If you can't, use an IIFE or one namespace object, and always declare your names.
Final definition: The global object is the one object that every classic script on a page shares. Modern code keeps its names off it, unless it shares something on purpose.
Up next: The this keyword, which explains what this means in each call.