What they do: Three ways to declare variables with different scoping rules, hoisting behavior, and reassignment permissions.

The Core Differences:

1. Scope Boundaries

function test() {
    if (true) {
        var a = 1;    // Function scoped
        let b = 2;    // Block scoped  
        const c = 3;  // Block scoped
    }
    console.log(a); // 1 (accessible)
    console.log(b); // ReferenceError
    console.log(c); // ReferenceError
}

2. Hoisting Behavior

console.log(x); // undefined (hoisted, initialized)
console.log(y); // ReferenceError (hoisted, not accessible)
console.log(z); // ReferenceError (hoisted, not accessible)
 
var x = 1;
let y = 2;
const z = 3;

3. Reassignment Rules

var a = 1;
a = 2;        // ✅ Allowed
 
let b = 1;
b = 2;        // ✅ Allowed
 
const c = 1;
c = 2;        // ❌ TypeError: Assignment to constant variable

Mental Model for Memory:

var: Function Container

function outer() {
    // var lives here (function scope)
    if (true) {
        var x = 1; // Hoisted to function level
    }
    // x is accessible here
}

let/const: Block Container

function outer() {
    if (true) {
        let y = 1;    // Lives only in this block
        const z = 2;  // Lives only in this block
    }
    // y and z don't exist here
}

Interview Gotchas:

Gotcha 1: Redeclaration

var x = 1;
var x = 2; // ✅ Allowed (overwrites)
 
let y = 1;
let y = 2; // ❌ SyntaxError: Identifier 'y' has already been declared
 
const z = 1;
const z = 2; // ❌ SyntaxError: Identifier 'z' has already been declared

Gotcha 2: Temporal Dead Zone

javascript

console.log(a); // undefined
console.log(b); // ReferenceError: Cannot access 'b' before initialization
 
var a = 1;
let b = 2;

Gotcha 3: Loop iterations

for (var i = 0; i < 3; i++) {
    setTimeout(() => console.log(i)); // 3, 3, 3
}
 
for (let i = 0; i < 3; i++) {
    setTimeout(() => console.log(i)); // 0, 1, 2
}

Gotcha 4: const with objects

const obj = { name: "John" };
obj.name = "Jane"; // ✅ Allowed (mutating content)
obj.age = 25;      // ✅ Allowed (mutating content)
obj = {};          // ❌ TypeError (reassigning variable)

Gotcha 5: Function vs block scope mixing

function test() {
    var x = 1;
    
    if (true) {
        var x = 2; // Same variable (function scoped)
        let y = 3; // New variable (block scoped)
    }
    
    console.log(x); // 2 (overwritten)
    console.log(y); // ReferenceError
}

The Decision Tree:

When you see variable declaration:

  1. Is it var?
    • Scope: Goes to nearest function (or global)
    • Hoisting: undefined initially
    • Can redeclare/reassign
  2. Is it let?
    • Scope: Stays in nearest block {}
    • Hoisting: In temporal dead zone
    • Can reassign, can’t redeclare
  3. Is it const?
    • Scope: Stays in nearest block {}
    • Hoisting: In temporal dead zone
    • Can’t reassign or redeclare
    • Must initialize when declared

Memory Address Mental Model:

var: One address per function

javascript

function test() {
    // Address 0x100 reserved for 'i'
    for (var i = 0; i < 3; i++) {
        // All references point to 0x100
    }
    // i still exists at 0x100
}

let: New address per block

javascript

function test() {
    for (let i = 0; i < 3; i++) {
        // Iteration 1: i at 0x200
        // Iteration 2: i at 0x300  
        // Iteration 3: i at 0x400
    }
    // All addresses freed, i doesn't exist
}

Bottom line: var hoists to function scope with undefined, let/const hoist to block scope in temporal dead zone. var allows redeclaration, let allows reassignment, const allows neither. Think of var as “function container” and let/const as “block container” for memory addresses.


That famous question :

What you think happens:

javascript

// You write:
for (var i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 100);
}
 
// JavaScript actually sees:
var i; // Hoisted to function/global scope
for (i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 100);
}

YES! The var i gets hoisted OUT of the for-block to the next scope up (usually function or global).

Why setTimeout Takes Reference (Not Value):

The Key: Closures capture variables by reference (memory address), not by value.

javascript

var i; // Memory address: 0x100
for (i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 100); // All point to same address 0x100
}
// When callbacks execute, they all read from 0x100 → final value is 3

Visual Memory Model:

Memory Address 0x100: [i = 3] ← All setTimeout callbacks point here
Callback1: () => console.log(i) → reads 0x100 → 3
Callback2: () => console.log(i) → reads 0x100 → 3  
Callback3: () => console.log(i) → reads 0x100 → 3

let Creates NEW Variables Each Iteration:

What JavaScript does with let:

javascript

for (let i = 0; i < 3; i++) {
    // JavaScript creates NEW 'i' for each iteration
    setTimeout(() => console.log(i), 100);
}
 
// Becomes conceptually:
{
    let i = 0; // Address 0x200
    setTimeout(() => console.log(i), 100); // Points to 0x200
}
{
    let i = 1; // Address 0x300 (NEW variable!)
    setTimeout(() => console.log(i), 100); // Points to 0x300
}
{
    let i = 2; // Address 0x400 (NEW variable!)
    setTimeout(() => console.log(i), 100); // Points to 0x400
}

Visual Memory Model:

Memory Address 0x200: [i = 0] ← Callback1 points here
Memory Address 0x300: [i = 1] ← Callback2 points here
Memory Address 0x400: [i = 2] ← Callback3 points here

The Complete Mental Model:

Rule 1: Scope Determines Where Variables Live

  • var: Function/global scope (hoisted out of blocks)
  • let/const: Block scope (stays in nearest {})

Rule 2: Closures Capture by Reference

  • Functions remember the memory address of variables
  • Not the value at the time of creation

Rule 3: let in Loops Creates New Variables

  • Each iteration gets fresh memory address
  • Each closure captures different address

Interview Variations They’ll Test:

Variation 1: Nested functions

javascript

function outer() {
    for (var i = 0; i < 3; i++) {
        function inner() {
            setTimeout(() => console.log(i), 100);
        }
        inner();
    }
}
// Still prints 3, 3, 3 (same var i reference)

Variation 2: Different scopes

javascript

function test() {
    for (var i = 0; i < 3; i++) {
        setTimeout(() => console.log(i), 100);
    }
    console.log("i outside loop:", i); // 3 (accessible!)
}
 
function test2() {
    for (let i = 0; i < 3; i++) {
        setTimeout(() => console.log(i), 100);
    }
    console.log("i outside loop:", i); // ReferenceError!
}

Variation 3: IIFE solution

javascript

for (var i = 0; i < 3; i++) {
    (function(j) { // Creates new scope with parameter j
        setTimeout(() => console.log(j), 100);
    })(i); // Pass current value of i
}
// Prints 0, 1, 2 (each j has different value)

Bottom Line Mental Model:

  1. var = hoisted to function scope, ONE variable shared
  2. let/const = block scope, NEW variable each iteration
  3. Closures = capture memory address, not value
  4. setTimeout = executes later, reads current value at that address