Chapter 1.2☕ 12 min read

Variables & Hoisting

var ek purana dost hai jo kabhi nahi jaata — aur yahi problem hai.

01var, let, const — The Basics

JavaScript has three ways to declare variables, and understanding the difference between them is non-negotiable:

var name = "Sai";      // function scope, hoisted, redeclarable
let city = "Hyd";      // block scope, TDZ, not redeclarable
const PI = 3.14;       // block scope, TDZ, not reassignable

var name = "Rahul";    // ✅ OK — var allows redeclaration
city = "Mumbai";       // ✅ OK — let allows reassignment
// PI = 3.15;          // ❌ TypeError: Assignment to constant variable

Quick mental model:

var   → "I do whatever I want, wherever I want"
let   → "I stay inside my { } but you can change my value"
const → "I stay inside my { } AND you cannot change my value"
const does not mean immutable. It means the binding cannot be reassigned. The object itself can still be mutated: const obj = {a:1}; obj.a = 2; works fine. Only obj = {} would throw. For true immutability, use Object.freeze().
02Scope — Function vs Block

"Scope" means where a variable is visible and accessible. JS has three scope levels:

// var leaks out of blocks
if (true) {
  var leaked = "I escaped!";
  let safe   = "I stay here";
}
console.log(leaked); // "I escaped!" ← surprise!
console.log(safe);   // ReferenceError ← expected
// var respects function boundary
function myFn() {
  var localVar = "inside";
}
console.log(localVar); // ReferenceError — can't escape function

Three scope levels:

// 1. Global scope — declared outside any function/block
var globalVar = "everywhere";

// 2. Function scope — declared inside a function
function fn() { var funcVar = "inside fn only"; }

// 3. Block scope — inside { } (if, for, while, etc.)
if (true) { let blockVar = "inside block only"; }
Angular SSR note: In Angular components, always use let or const. var can leak across @if blocks in templates and cause subtle bugs that only appear in SSR. The Angular style guide explicitly forbids var.
03Hoisting — The Invisible Hand

When JS runs your code, it does two passes:

// Pass 1 (Compilation): Collects all var and function declarations
// Pass 2 (Execution):   Runs the code top to bottom

console.log(x); // undefined (not ReferenceError!) — var hoisted
var x = 5;
console.log(x); // 5

// Equivalent to what JS actually runs:
var x;           // ← hoisted here by JS engine
console.log(x);  // undefined
x = 5;
console.log(x);  // 5

Function declarations are fully hoisted — the entire function body moves up:

greet(); // ✅ Works! — function declaration fully hoisted
function greet() { console.log("Hyderabad!"); }

sayHi(); // ❌ TypeError: sayHi is not a function
var sayHi = function() { console.log("Hi!"); }; // only var hoisted, not fn
📋 Key insight: Hoisting is why you can call a function before its definition in JS — but only if it's a function declaration (function foo() {}), not a function expression (var foo = function() {}). The expression only hoists the var, leaving undefined.
04Temporal Dead Zone (TDZ)

Temporal Dead Zone (TDZ) is the period between entering a block and reaching the let/const declaration line:

console.log(a); // undefined — var has no TDZ
var a = 1;

console.log(b); // ❌ ReferenceError: Cannot access 'b' before initialization
let b = 2;

// TDZ exists from { to the let line:
{
  // ← TDZ for c starts here
  console.log(c); // ❌ ReferenceError
  let c = 3;      // ← TDZ ends here
}

Even typeof throws in TDZ:

console.log(typeof undeclaredVar); // "undefined" — safe for undeclared
console.log(typeof myLet);          // ❌ ReferenceError! (not "undefined")
let myLet = 5;
TDZ is actually a feature, not a bug. It makes "use before declare" errors visible immediately instead of silently returning undefined (like var does). This catches real bugs early. Every let/const is hoisted — just not initialized until its line.
05Best Practices — When to Use What

The decision tree is simple:

// ✅ Default choice: const
const API_URL = "https://api.example.com";
const config = { theme: "dark" };

// ✅ When reassignment is needed: let
let retryCount = 0;
retryCount++;

for (let i = 0; i < items.length; i++) {
  // let creates a new binding per iteration — closures work correctly
}

// ✅ Angular-specific patterns
const userService = inject(UserService);  // never reassigned
const user = computed(() => userService.currentUser());
let isLoading = signal(false);             // signal itself is const, .set() mutates internal

// ❌ Never use var
var oldStyle = "please no";
Why let in for loops matters: With var, all loop iterations share one variable. With let, each iteration gets its own copy. This is critical for closures inside loops — a classic interview question covered in Chapter 1.3 (Functions & Closures).

Lo kar liya — Key Points:

  • const by default, let when reassignment needed, never var
  • var is function-scoped; let/const are block-scoped
  • var is hoisted AND initialized (undefined); let/const are hoisted but NOT initialized (TDZ)
  • ✅ TDZ throws ReferenceError — var gives undefined — know the difference for interviews
  • const doesn't mean immutable — object properties can still change
  • ✅ Function declarations are fully hoisted; function expressions are NOT
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase