Handbooks / JavaScript / Chapter 2

Scope and Closures

7 pages · ~5 min✓ Reviewed

Builds on Types and Coercion. Next up: this and Prototypes.

JavaScriptPage 1

Variables and Scope Mental Model

1. var, let, const

varletconst
Scopefunctionblockblock
Reassignyesyesno
  • A block is any { }: if, for, or a bare block
  • const locks the binding, not the value
javascript
const a = [];
a.push(1);  // works
a = [];     // TypeError

mutating is fine, rebinding throws

2. Lexical scope and lookup

  • Lexical = visibility fixed by where code is written
  • Not by where the function is called
  1. Current scope
  2. Enclosing scopesoutward
  3. Global
  4. ReferenceErrornot found

3. Global object

Global declarationOn globalThis?
varyes
function declarationyes
let / constno
Remember

Top-level let x = 1 gives globalThis.x as undefined; only var and functions attach.

JavaScriptPage 2

Hoisting and the Temporal Dead Zone

1. What hoisting does

  • Hoisting: declarations are registered at scope creation, before code runs
  • The name exists from the top of its scope
  • var is hoisted and initialised to undefined
javascript
console.log(x);
var x = 5;

no error: x exists, value undefined

output
undefined

2. let, const and functions

  • let/const are hoisted but uninitialised
  • Access before the declaration line → ReferenceError
  • That gap is the TDZ (temporal dead zone)
Used before its lineResult
var xundefined
let / constReferenceError
function declarationworks, body hoisted
var f = function(){}TypeError: not a function
typeof on TDZ variableReferenceError
typeof on undeclared name'undefined'

3. Interview Point

  • Does let hoist? Yes, but it stays in the TDZ until its line runs
  • Why TypeError on an early call? var f is undefined, not a function
  • typeof on a TDZ variable? Throws; an undeclared name gives 'undefined'
JavaScriptPage 3

Closures: How They Work and Where to Use Them

1. What a closure is

  • Closure = function + reference to the scope where it was created
  • Keeps that scope alive after the outer function returns
  • Captures by reference, not value: later changes are seen

2. Private state and factories

  • n is private: only the returned function can touch it
  • Factory: outer arg fixed now, inner arg later (partial application)
javascript
function counter(){ let n = 0; return () => ++n }
const c = counter();
console.log(c(), c());
function mul(a){ return b => a*b }
const double = mul(2);
console.log(double(5), c.n);
output
1 2
10 undefined

3. Common uses

UseClosure remembers
Callbacks, event handlersthe data they need
Memoisationthe cache object
once / debouncea called flag / timer id
Shared, not copied

The inner function reads the live variable. Change it outside and the closure sees the new value

JavaScriptPage 4

The Loop Closure Pitfall

1. The Bug

  • All callbacks share one function-scoped i
  • Callbacks run later, after the loop ends, when i is 3
javascript
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i))
}
output
3
3
3

2. Three Fixes

FixHowPrints
let in loop headfresh binding copy per iteration0 1 2
IIFE (legacy)(function(j){ setTimeout(()=>console.log(j)) })(i)0 1 2
forEach / mapcallback parameter is a new scope per element0 1 2

3. Same Trap: Click Handlers

  • Handlers attached in a var loop all read the final index
  • Fix the same way: let or an IIFE
Remember

var = one shared binding; let in the loop head = one binding per iteration

JavaScriptPage 5

IIFE and Modules

1. IIFE and module pattern

  • (function(){ ... })(); runs once, private scope
  • Wrapping ( ) make it an expression, not a declaration
  • Module pattern: IIFE returns public methods; vars stay private via closures
javascript
const counter = (function () {
  let n = 0;
  return { inc: () => ++n };
})();
counter.inc(); // n is unreachable

2. ES modules

  • Own module scope: top-level vars are not global
  • Strict mode by default
  • Loaded once, then cached
  • Exports are live bindings: importers see new values of an exported let
  • Importers cannot assign to an import
javascript
// a.js
export let count = 0;
export function inc() { count++; }
// b.js
import { count, inc } from './a.js';
inc();        // count is now 1
count = 5;    // TypeError

3. What to use

UseWhen
IIFEold script-tag code
ES modulesnew code
constdefault
letwhen reassigning
varnever
JavaScriptPage 6

Scope and Closures Cheat Sheet

1. Declarations

varletconst
Scopefunctionblockblock
Hoisted asundefinedTDZTDZ
Redeclareyesnono
Reassignyesyesno
  • const first
  • let only if reassigned
  • avoid var

2. Hoisting

FormUsed before its line
varundefined
let / constReferenceError (TDZ)
function declarationworks, fully hoisted
function expressionTypeError, only the variable is hoisted

3. Closures and the Loop Fix

  • Inner function keeps a reference to outer variables
  • Each call of the outer function makes a new independent scope
  • let in the loop head gives per-iteration binding
  • var needs an IIFE: (function(j){ ... })(i)
javascript
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i));
}
output
0
1
2
Classic bug

var in the loop head shares one i, so the callbacks print 3 3 3

JavaScriptPage 7

Check yourself

What does this print?
  • 3 3 3
  • One function-scoped i is shared and is 3 when the callbacks run
  • With let i, each iteration gets its own binding: 0 1 2
for (var i = 0; i < 3; i++)
  setTimeout(() => console.log(i))
Why does this throw?
  • ReferenceError
  • y is in the temporal dead zone until its declaration line
  • typeof is only safe on names never declared
console.log(typeof y)
let y = 1
Which line throws, and what error?
  • Only the last line: TypeError
  • push mutates the value, and the binding stays the same
  • a = [] reassigns the binding, which const forbids
const a = []
a.push(1)
a = []
What does this print?
  • 2
  • A closure captures the variable by reference, not its value at creation
let n = 1
const f = () => n
n = 2
console.log(f())