Scope and Closures
var, let, and const
Section titled “var, let, and const”JavaScript has three declaration keywords, each with different scoping rules:
| Keyword | Scope | Hoisted | Reassignable | Re-declarable |
|---|---|---|---|---|
var | Function | Yes (as undefined) | Yes | Yes |
let | Block | Yes (TDZ) | Yes | No |
const | Block | Yes (TDZ) | No | No |
Block scope means the variable only exists between the nearest {} braces. Function scope means the variable lives for the entire function body, regardless of any inner blocks.
function example() { if (true) { var x = 1; // function-scoped — leaks outside the if let y = 2; // block-scoped — stays inside the if const z = 3; // block-scoped } console.log(x); // 1 — visible here console.log(y); // ReferenceError: y is not defined}Hoisting
Section titled “Hoisting”Declarations are hoisted — moved to the top of their scope by the engine before execution.
var hoisting initialises the variable as undefined immediately. let/const are hoisted but stay in a Temporal Dead Zone (TDZ) — any access before the declaration line throws a ReferenceError.
console.log(a); // undefined — var is hoisted and initialisedvar a = 5;
console.log(b); // ReferenceError — let is in TDZlet b = 10;Function declarations are also fully hoisted — you can call them before the line where they are written. Function expressions (assigned to variables) are not.
Lexical scope
Section titled “Lexical scope”Scope in JavaScript is lexical (also called static): a function’s scope is determined by where it is written in the source, not where it is called from. An inner function can always read variables from its surrounding outer scopes.
const prefix = 'LOG';
function outer() { const level = 'INFO';
function inner(msg) { // inner can read both prefix (module scope) and level (outer scope) console.log('[' + prefix + ':' + level + '] ' + msg); }
inner('started');}
outer(); // [LOG:INFO] startedClosures
Section titled “Closures”A closure is a function that retains access to the variables of its enclosing scope even after that outer function has returned. The inner function does not copy the values — it holds a live reference to the variables themselves.
A classic use case is a counter factory:
function makeCounter(start) { let count = start;
return { increment: function() { count += 1; }, decrement: function() { count -= 1; }, value: function() { return count; }, };}
const counter = makeCounter(0);counter.increment();counter.increment();counter.increment();counter.decrement();console.log(counter.value()); // 2count lives in makeCounter’s scope. Every function returned from makeCounter holds a reference to that same count variable.
Runnable closure demo
Section titled “Runnable closure demo”Note that c1 and c2 are completely independent — each call to makeCounter creates a fresh count variable in a new scope, and each returned object closes over its own copy.