Knowing where a name is visible explains a whole class of 'undefined' and ReferenceError bugs.
JavaScript uses lexical scope: where a variable is visible is decided by where it is written in the source, not by how functions are called. Scopes nest, and a lookup walks outward from the innermost scope until it finds the name — or throws ReferenceError.
The kinds of scope:
- Global — names visible everywhere.
globalThisis the global object in every environment. - Module — each ES module has its own top-level scope; nothing leaks to the global unless you put it there.
- Function — parameters and everything declared inside.
- Block —
let,constandclassdeclarations inside{ }.
Hoisting describes the fact that declarations are processed when a scope is entered, before any code in it runs:
- Function declarations are hoisted completely; you can call them before their line.
vardeclarations are hoisted and initialised toundefined, so reading one early silently givesundefined.varalso ignores blocks and is scoped to the whole function.let,constandclassare hoisted but not initialised. Touching them before the declaration throws aReferenceError; that window is the temporal dead zone (TDZ).
An inner declaration with the same name shadows the outer one within its scope. That is legal but can be confusing; linters can flag it.
In non-module scripts, a top-level var or function declaration becomes a property of the global object, and assigning to an undeclared name creates a global in sloppy mode. Modules and strict mode turn the latter into an error — another reason to write modules.
Example
console.log(hoisted()); // "works": declarations are hoisted
function hoisted() { return "works"; }
console.log(v); // undefined (var hoisted, initialised to undefined)
var v = 1;
try {
console.log(l); // TDZ
} catch (e) {
console.log(e.name); // ReferenceError
}
let l = 2;
function scopes() {
if (true) {
var fnScoped = "var"; // visible in the whole function
let blockScoped = "let"; // only inside this block
}
console.log(fnScoped); // "var"
console.log(typeof blockScoped); // "undefined" (not in scope)
}
scopes();
const name = "outer";
{ const name = "inner"; console.log(name); } // inner (shadowing)
console.log(name); // outerRemember
- Scope is lexical: decided by where code is written
varis function-scoped and reads asundefinedearly;let/constthrow in the TDZ- Function declarations are fully hoisted; function expressions are not
Watch out for
- Relying on
varinside anifor loop and expecting it to stay in that block.