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 variableMental 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 declaredGotcha 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:
- Is it var?
- Scope: Goes to nearest function (or global)
- Hoisting: undefined initially
- Can redeclare/reassign
- Is it let?
- Scope: Stays in nearest block
{} - Hoisting: In temporal dead zone
- Can reassign, can’t redeclare
- Scope: Stays in nearest block
- Is it const?
- Scope: Stays in nearest block
{} - Hoisting: In temporal dead zone
- Can’t reassign or redeclare
- Must initialize when declared
- Scope: Stays in nearest block
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 3Visual 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:
- var = hoisted to function scope, ONE variable shared
- let/const = block scope, NEW variable each iteration
- Closures = capture memory address, not value
- setTimeout = executes later, reads current value at that address
