⚑ JavaScript Fundamentals

Deep dive into Execution Context, Hoisting, Closures, this, Prototypes, ES6 Classes & Inheritance

πŸ“¦ Execution Context

The Execution Context is the environment where JavaScript code is evaluated and executed. It's the foundation for understanding closures, hoisting, scope, and this.

What's Inside an Execution Context?

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ EXECUTION CONTEXT β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ 1. Variable Environment β”‚ β”‚ β€’ Variables (var, let, const) β”‚ β”‚ β€’ Function declarations β”‚ β”‚ β€’ Arguments object (for functions) β”‚ β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ 2. Lexical Environment β”‚ β”‚ β€’ Reference to outer environment (parent scope) β”‚ β”‚ β€’ Enables scope chain lookups β”‚ β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ 3. This Binding β”‚ β”‚ β€’ What `this` refers to in this context β”‚ β”‚ β€’ Determined by HOW the function is called β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Types of Execution Context

TypeDescriptionWhen Created
GlobalCreated when JS file starts, only ONE per programScript starts
FunctionCreated every time a function is calledFunction invoked
EvalCreated when eval() is used (avoid using)eval() called

Two Phases of Execution Context

Phase 1: Creation (Memory Allocation)

  • Memory allocated for variables & functions
  • var β†’ initialized to undefined
  • let/const β†’ uninitialized (TDZ)
  • Function declarations β†’ stored entirely in memory
  • this binding determined

Phase 2: Execution (Code Runs)

  • Code executed line by line
  • Variables assigned actual values
  • Functions called (new contexts created)
  • Expressions evaluated
  • Callables invoked

Visualizing Both Phases

var name = "Sandeep";
let age = 25;

function greet() {
    console.log("Hello " + name);
}

greet();

// ═══════════════════════════════════════════════════════════════
// PHASE 1: CREATION PHASE (Before any code runs)
// ═══════════════════════════════════════════════════════════════
// 
// Global Execution Context Memory:
// β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
// β”‚ name          β†’ undefined           β”‚  (var gets undefined)
// β”‚ age           β†’ <uninitialized>     β”‚  (let is in TDZ)
// β”‚ greet         β†’ function(){...}     β”‚  (full function stored)
// β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
// 
// ═══════════════════════════════════════════════════════════════
// PHASE 2: EXECUTION PHASE (Code runs line by line)
// ═══════════════════════════════════════════════════════════════
// 
// Line 1: name = "Sandeep"
// β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
// β”‚ name          β†’ "Sandeep"           β”‚  (updated!)
// β”‚ age           β†’ <uninitialized>     β”‚
// β”‚ greet         β†’ function(){...}     β”‚
// β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
// 
// Line 2: age = 25
// β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
// β”‚ name          β†’ "Sandeep"           β”‚
// β”‚ age           β†’ 25                  β”‚  (now accessible!)
// β”‚ greet         β†’ function(){...}     β”‚
// β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
// 
// Line 8: greet() is called β†’ New Function Execution Context!

The Call Stack

The Call Stack is how JavaScript keeps track of which execution context is running. Only ONE executes at a time!

function first() {
    console.log("First - start");
    second();
    console.log("First - end");
}

function second() {
    console.log("Second - start");
    third();
    console.log("Second - end");
}

function third() {
    console.log("Third");
}

first();

// Output:
// First - start
// Second - start
// Third
// Second - end
// First - end

// ═══════════════════════════════════════════════════════════════
// Call Stack Visualization (step by step)
// ═══════════════════════════════════════════════════════════════
//
// Step 1: first() called
// β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
// β”‚ first() Context β”‚
// β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
// β”‚ Global Context  β”‚
// β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
//
// Step 2: second() called (inside first)
// β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
// β”‚ second() Contextβ”‚
// β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
// β”‚ first() Context β”‚  ← Paused, waiting
// β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
// β”‚ Global Context  β”‚
// β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
//
// Step 3: third() called (inside second)
// β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
// β”‚ third() Context β”‚  ← Currently executing (top of stack)
// β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
// β”‚ second() Contextβ”‚  ← Paused, waiting
// β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
// β”‚ first() Context β”‚  ← Paused, waiting
// β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
// β”‚ Global Context  β”‚  ← Always at bottom
// β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
//
// Step 4: third() finishes β†’ POPPED off stack, memory freed!
// Step 5: second() resumes β†’ finishes β†’ POPPED
// Step 6: first() resumes β†’ finishes β†’ POPPED
Key Insight:
  • Creation Phase = Memory allocation for all declarations
  • Execution Phase = Running code line by line
  • Only ONE execution context runs at a time (JavaScript is single-threaded)

πŸ”„ Hoisting & Temporal Dead Zone

Hoisting is JavaScript's behavior where variable and function declarations are processed during the Creation Phase, before any code executes.

⚠️ Important Clarification: "Moving to the top" is just a metaphor! Nothing actually moves. What really happens is memory allocation during the Creation Phase.
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ THE REALITY β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ Creation Phase: β”‚ β”‚ β€’ JS engine scans ALL declarations (var, let, const, function) β”‚ β”‚ β€’ Allocates memory for them β”‚ β”‚ β€’ var β†’ gets undefined β”‚ β”‚ β€’ let/const β†’ marked as "uninitialized" (TDZ) β”‚ β”‚ β€’ function declarations β†’ entire function stored β”‚ β”‚ β”‚ β”‚ Execution Phase: β”‚ β”‚ β€’ Code runs line by line β”‚ β”‚ β€’ Variables get their actual values when assignment is reached β”‚ β”‚ β”‚ β”‚ So "hoisting" = memory allocation happens in creation phase β”‚ β”‚ NOT = code physically moving β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Hoisting Behavior by Type

DeclarationHoisted?Initial ValueAccessible Before?
varβœ… Yesundefinedβœ… Yes (returns undefined)
letβœ… YesUninitialized❌ TDZ Error
constβœ… YesUninitialized❌ TDZ Error
function declarationβœ… YesEntire functionβœ… Fully works!
function expressionAs variableSame as var/let/constSame as variable type
class declarationβœ… YesUninitialized❌ TDZ Error

var Hoisting - The Classic Example

console.log(message);  // undefined (NOT an error!)
var message = "Hello World";
console.log(message);  // "Hello World"

// ═══════════════════════════════════════════════════════════════
// What actually happens:
// ═══════════════════════════════════════════════════════════════
//
// CREATION PHASE:
// Memory: { message: undefined }
//
// EXECUTION PHASE:
// Line 1: console.log(message) β†’ prints undefined (already in memory!)
// Line 2: message = "Hello World" β†’ updates memory
// Line 3: console.log(message) β†’ prints "Hello World"

Why var Gets undefined But let/const Get Errors

// This is a DESIGN CHOICE by JavaScript:

// var - Legacy behavior (ES5 and before)
// Initialize to undefined for "safety" - prevents crashes, but hides bugs
console.log(a);  // undefined
var a = 10;

// let/const - Modern behavior (ES6)
// Leave uninitialized to CATCH bugs early - fail fast philosophy
console.log(b);  // ReferenceError: Cannot access 'b' before initialization
let b = 20;

// The error message says "before initialization" NOT "b is not defined"
// This proves let IS hoisted (exists) but can't be accessed yet!

Temporal Dead Zone (TDZ)

The TDZ is the period between entering a scope and the actual declaration line.

{ // Block starts here β”‚ β”‚ ← TDZ STARTS (variable exists in memory but can't be accessed) β”‚ β”‚ console.log(age); // ReferenceError! Still in TDZ β”‚ console.log(age); // ReferenceError! Still in TDZ β”‚ console.log(age); // ReferenceError! Still in TDZ β”‚ β”‚ ← TDZ ENDS here β–Ό let age = 25; // Declaration + Initialization console.log(age); // 25 (works fine now!) }
// TDZ in action - tricky example
let x = 10;

function example() {
    console.log(x);  // ReferenceError!
    let x = 20;      // This shadows outer x, but TDZ applies
}

example();

// You might expect 10 (from outer x), but NO!
// The inner let x creates TDZ from function start to declaration line
// So accessing x before line 5 throws ReferenceError

Function Hoisting - The Special Case

// Function DECLARATIONS are fully hoisted!
greet();  // "Hello!" - Works BEFORE declaration!

function greet() {
    console.log("Hello!");
}

// ═══════════════════════════════════════════════════════════════
// Why? In creation phase:
// Memory: { greet: function greet() { console.log("Hello!"); } }
// The ENTIRE function is stored, not just the name
// ═══════════════════════════════════════════════════════════════
// Function EXPRESSIONS follow their variable type!

sayHi();  // TypeError: sayHi is not a function

var sayHi = function() {
    console.log("Hi!");
};

// In creation phase:
// Memory: { sayHi: undefined }
// Calling undefined() throws TypeError

// ═══════════════════════════════════════════════════════════════

sayHello();  // ReferenceError: Cannot access 'sayHello' before initialization

let sayHello = function() {
    console.log("Hello!");
};

// In creation phase:
// Memory: { sayHello: <uninitialized> }
// Accessing before declaration triggers TDZ error

The Classic Loop Gotcha (var vs let)

// ═══════════════════════════════════════════════════════════════
// Problem: Using var in loops with callbacks
// ═══════════════════════════════════════════════════════════════

for (var i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i);
    }, 100);
}

// Expected: 0, 1, 2
// Actual:   3, 3, 3

// WHY?
// var is FUNCTION-scoped, not block-scoped
// Only ONE variable 'i' exists for the entire loop
// By the time setTimeout callbacks run, the loop is done
// And i = 3 (the exit condition)

// ═══════════════════════════════════════════════════════════════
// Solution 1: Use let (block-scoped)
// ═══════════════════════════════════════════════════════════════

for (let i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i);
    }, 100);
}

// Output: 0, 1, 2 βœ“
// Each iteration gets its OWN 'i' variable (block scope)

// ═══════════════════════════════════════════════════════════════
// Solution 2: IIFE (Immediately Invoked Function Expression)
// ═══════════════════════════════════════════════════════════════

for (var i = 0; i < 3; i++) {
    (function(j) {
        setTimeout(function() {
            console.log(j);
        }, 100);
    })(i);  // Pass current i as j, creating new closure scope
}

// Output: 0, 1, 2 βœ“

πŸ”— Lexical Environment & Scope Chain

Lexical means "relating to the words/text" - in programming, it means where the code is physically written.

Lexical Environment = Local Memory + Reference to Parent Environment

Scope Chain Lookup

When JavaScript needs a variable: 1. Look in CURRENT scope ↓ Not found? 2. Look in PARENT scope ↓ Not found? 3. Look in GRANDPARENT scope ↓ Keep going up... 4. Look in GLOBAL scope ↓ Not found? 5. ReferenceError: variable is not defined

Complete Scope Chain Example

var globalVar = "I'm global";

function outer() {
    var outerVar = "I'm in outer";
    
    function inner() {
        var innerVar = "I'm in inner";
        
        // Scope chain lookup in action:
        console.log(innerVar);   // "I'm in inner"  ← Found in own scope
        console.log(outerVar);   // "I'm in outer"  ← Found in parent (outer)
        console.log(globalVar);  // "I'm global"    ← Found in grandparent (global)
    }
    
    inner();
}

outer();

// ═══════════════════════════════════════════════════════════════
// Visualizing the Scope Chain:
// ═══════════════════════════════════════════════════════════════
//
//                     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
//                     β”‚   Global Scope       β”‚
//                     β”‚   globalVar = "..."  β”‚
//                     β”‚   outer = function   β”‚
//                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
//                                β”‚ parent reference
//                     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
//                     β”‚   outer() Scope      β”‚
//                     β”‚   outerVar = "..."   β”‚
//                     β”‚   inner = function   β”‚
//                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
//                                β”‚ parent reference
//                     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
//                     β”‚   inner() Scope      β”‚
//                     β”‚   innerVar = "..."   β”‚
//                     β”‚   (lookup starts)    β”‚
//                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Lexical Scope = WHERE Code is Written (Not Where Called!)

var name = "Global";

function printName() {
    console.log(name);  // Which name will this print?
}

function wrapper() {
    var name = "Local";
    printName();  // Calling printName from inside wrapper
}

wrapper();

// Output: "Global" (NOT "Local"!)

// ═══════════════════════════════════════════════════════════════
// Why?
// ═══════════════════════════════════════════════════════════════
//
// printName is DEFINED in Global scope
// So its parent lexical environment is GLOBAL
// 
// When printName looks for 'name':
// 1. Check own scope β†’ not found
// 2. Check parent (GLOBAL, where it was WRITTEN) β†’ found "Global"
//
// It doesn't matter that printName was CALLED from wrapper
// Lexical scope is about WHERE CODE IS WRITTEN!

⚠️ Scope Chain Does NOT Work Downward

function parent() {
    
    function child() {
        var childVar = "I'm in child";
    }
    
    child();
    console.log(childVar);  // ReferenceError: childVar is not defined
}

parent();

// ═══════════════════════════════════════════════════════════════
// Why the error?
// ═══════════════════════════════════════════════════════════════
//
// The REAL reason: child()'s execution context is GONE from memory!
//
// Timeline:
// 1. parent() starts executing
// 2. child() is called β†’ new execution context pushed to stack
// 3. childVar is created in child's context
// 4. child() finishes β†’ context POPPED from stack and GARBAGE COLLECTED
// 5. parent() continues β†’ tries to access childVar
// 6. But childVar doesn't exist anymore! Memory was freed!
//
// So the explanation is simple:
// - Child's execution context is destroyed when it finishes
// - Its variables are no longer in memory
// - Parent can't access what doesn't exist
//
// (Remember: only ONE execution context runs at a time)
// (Parent was PAUSED while child ran, by the time parent resumes, child is gone)

Variable Shadowing

var name = "Global Name";

function outer() {
    var name = "Outer Name";  // Shadows global name
    
    function inner() {
        var name = "Inner Name";  // Shadows outer name
        console.log(name);  // "Inner Name"
    }
    
    inner();
    console.log(name);  // "Outer Name"
}

outer();
console.log(name);  // "Global Name"

// Each scope has its own 'name' - they don't interfere with each other
// Inner name "shadows" (hides) the outer ones

Block Scope with let/const

// let/const create new scope for each block {}

if (true) {
    let blockVar = "I exist only in this block";
    const BLOCK_CONST = "Me too";
    
    console.log(blockVar);     // Works
    console.log(BLOCK_CONST);  // Works
}

console.log(blockVar);     // ReferenceError!
console.log(BLOCK_CONST);  // ReferenceError!

// var does NOT respect block scope
if (true) {
    var notBlocked = "I escape the block!";
}
console.log(notBlocked);  // "I escape the block!"
Scope Chain Rules Summary:
  • Lexical Scope - Determined by WHERE code is written, not where called
  • Upward Only - Can look UP to parents, never DOWN into children
  • Shadowing - Inner variable with same name hides outer one
  • Block Scope - let/const create new scope per block, var doesn't

πŸ”’ Closures

A closure is when a function remembers and accesses variables from its lexical environment even after the outer function has finished executing.

The Puzzle That Reveals Closures

function outer() {
    var secret = "hidden treasure";
    
    return function inner() {
        console.log(secret);
    };
}

var myFunc = outer();  // outer() finishes executing, should be gone!
myFunc();              // "hidden treasure" ← HOW?!

// ═══════════════════════════════════════════════════════════════
// The mystery:
// - outer() has finished executing
// - Its execution context was POPPED from the call stack
// - So 'secret' should be garbage collected, right?
// - But inner() can still access it!
// ═══════════════════════════════════════════════════════════════

How Closures Work

When inner() is CREATED inside outer(), it gets: inner = { code: function() { console.log(secret); }, [[Environment]]: REFERENCE to outer's lexical environment } This [[Environment]] reference keeps outer's variables ALIVE! Garbage Collector sees: "inner still has a reference to outer's environment" "Someone might need those variables!" "I WON'T free that memory!"

Closure = Function + Its Lexical Environment

// Every time you call createCounter(), you get a NEW closure
// Each closure has its own copy of 'count'

function createCounter() {
    var count = 0;  // This will be "remembered"
    
    return function() {
        count++;
        return count;
    };
}

var counter1 = createCounter();  // New closure with { count: 0 }
var counter2 = createCounter();  // Another closure with { count: 0 }

console.log(counter1());  // 1
console.log(counter1());  // 2
console.log(counter1());  // 3

console.log(counter2());  // 1 ← Different closure, different count!
console.log(counter2());  // 2

// counter1 and counter2 are completely independent
// They each "close over" their own 'count' variable

Closure Use Cases

1. Data Privacy / Encapsulation

function createBankAccount(initial) {
    // 'balance' is PRIVATE
    var balance = initial;
    
    return {
        deposit: function(amt) {
            balance += amt;
        },
        withdraw: function(amt) {
            if (amt <= balance) {
                balance -= amt;
            }
        },
        getBalance: function() {
            return balance;
        }
    };
}

var account = createBankAccount(1000);
account.deposit(500);
console.log(account.getBalance());  // 1500
console.log(account.balance);  // undefined!

2. Function Factory

function createMultiplier(factor) {
    // 'factor' is remembered
    return function(number) {
        return number * factor;
    };
}

var double = createMultiplier(2);
var triple = createMultiplier(3);
var quadruple = createMultiplier(4);

console.log(double(5));     // 10
console.log(triple(5));     // 15
console.log(quadruple(5));  // 20

3. Event Handlers with State

function createButtonHandler(buttonName) {
    var clickCount = 0;
    
    return function() {
        clickCount++;
        console.log(buttonName + " clicked " + clickCount + " times");
    };
}

var saveHandler = createButtonHandler('Save');
var deleteHandler = createButtonHandler('Delete');

saveHandler();    // "Save clicked 1 times"
saveHandler();    // "Save clicked 2 times"
deleteHandler();  // "Delete clicked 1 times" ← Separate counter!
saveHandler();    // "Save clicked 3 times"

The Loop Gotcha (Closure Edition)

// ═══════════════════════════════════════════════════════════════
// Common Mistake: All closures share the same variable!
// ═══════════════════════════════════════════════════════════════

function createFunctions() {
    var functions = [];
    
    for (var i = 0; i < 3; i++) {
        functions.push(function() {
            console.log(i);
        });
    }
    
    return functions;
}

var funcs = createFunctions();
funcs[0]();  // 3 (not 0!)
funcs[1]();  // 3 (not 1!)
funcs[2]();  // 3 (not 2!)

// WHY?
// All three functions close over the SAME 'i' variable
// By the time they run, the loop is done and i = 3

// ═══════════════════════════════════════════════════════════════
// Solution 1: Use let (creates new binding each iteration)
// ═══════════════════════════════════════════════════════════════

function createFunctions() {
    var functions = [];
    
    for (let i = 0; i < 3; i++) {  // let instead of var
        functions.push(function() {
            console.log(i);
        });
    }
    
    return functions;
}

var funcs = createFunctions();
funcs[0]();  // 0 βœ“
funcs[1]();  // 1 βœ“
funcs[2]();  // 2 βœ“

Memory Perspective on Closures

// ═══════════════════════════════════════════════════════════════
// Without closure: Memory is freed when function ends
// ═══════════════════════════════════════════════════════════════

function noClosureExample() {
    var data = "some data";
    console.log(data);
    // Function ends β†’ data is garbage collected
}

noClosureExample();
// 'data' no longer exists in memory

// ═══════════════════════════════════════════════════════════════
// With closure: Memory stays alive as long as closure exists
// ═══════════════════════════════════════════════════════════════

function closureExample() {
    var data = "some data";
    
    return function() {
        console.log(data);
    };
}

var closureFunc = closureExample();
// 'data' is STILL in memory! closureFunc holds a reference to it

closureFunc();  // "some data"

// To free the memory:
closureFunc = null;  // Now 'data' can be garbage collected

πŸ‘† The this Keyword

this is a special keyword that's determined by HOW a function is called, not where it's written.

Critical Difference:
  • Lexical Scope (closures): WHERE function is WRITTEN
  • this Binding: HOW function is CALLED

The 4 Rules of this Binding (Priority Order)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ RULE PRIORITY (highest to lowest) β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ 1. new Binding β†’ new func() β†’ new object β”‚ β”‚ 2. Explicit Binding β†’ call/apply/bind β†’ specified object β”‚ β”‚ 3. Implicit Binding β†’ obj.func() β†’ obj (before dot) β”‚ β”‚ 4. Default Binding β†’ func() β†’ window/undefined β”‚ β”‚ β”‚ β”‚ Check in this order: new > explicit > implicit > default β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
1

Default Binding

func() - Plain function call β†’ this = window/undefined

function showThis() {
    console.log(this);
}

showThis();  // window (non-strict) or undefined (strict mode)

"use strict";
function strictShowThis() {
    console.log(this);
}
strictShowThis();  // undefined
2

Implicit Binding

obj.func() - Method call β†’ this = object before the dot

var person = {
    name: "Sandeep",
    age: 25,
    greet: function() {
        console.log("Hello, I'm " + this.name);
    },
    getInfo: function() {
        console.log(this.name + " is " + this.age + " years old");
    }
};

person.greet();    // "Hello, I'm Sandeep"   - this = person
person.getInfo();  // "Sandeep is 25 years old" - this = person

// ⚠️ GOTCHA: Losing implicit binding
var greetFunc = person.greet;
greetFunc();  // "Hello, undefined" ← Lost binding!

// WHY?
// greetFunc() is a plain function call (no dot!)
// So it falls back to DEFAULT binding
3

Explicit Binding (call, apply, bind)

call/apply/bind β†’ this = specified object

function introduce(greeting, punctuation) {
    console.log(greeting + ", I'm " + this.name + punctuation);
}

var person1 = { name: "Sandeep" };
var person2 = { name: "Kumar" };

// call - Immediately invokes, args passed individually
introduce.call(person1, "Hello", "!");   // "Hello, I'm Sandeep!"
introduce.call(person2, "Hi", ".");      // "Hi, I'm Kumar."

// apply - Immediately invokes, args passed as array
introduce.apply(person1, ["Hey", "!!"]);  // "Hey, I'm Sandeep!!"

// bind - Returns NEW function with this permanently bound
var introduceSandeep = introduce.bind(person1);
introduceSandeep("Namaste", "πŸ™");  // "Namaste, I'm SandeepπŸ™"

// Even if you try to change this later, bind wins:
introduceSandeep.call(person2, "Test", "!");
// "Test, I'm Sandeep!" ← Still Sandeep, not Kumar!

⚠️ bind() Stores a REFERENCE, Not a Copy

var person = { name: "Sandeep", age: 25 };

function showInfo() {
    console.log(this.name + " is " + this.age + " years old");
}

var boundFunc = showInfo.bind(person);
boundFunc();  // "Sandeep is 25 years old"

// Now mutate the object
person.name = "Kumar";
person.age = 30;

boundFunc();  // "Kumar is 30 years old" ← Sees the mutation!

// WHY?
// bind() stores a REFERENCE to the object, not a copy
// When you mutate the object, boundFunc sees the changes
// Because it still points to the same object in memory

bind() Memory Considerations

bind() creates NEW function objects β†’ can cause memory issues:
  • Each bind() creates a new function object in memory
  • 1000 components with bound handlers = 1000 extra functions
  • Event listeners need stored references for cleanup
  • call/apply have no memory overhead (invoke immediately)
// ═══════════════════════════════════════════════════════════════
// Potential Memory Leak with Event Listeners
// ═══════════════════════════════════════════════════════════════

class BadExample {
    constructor(element) {
        this.element = element;
        // Creates NEW function each time - can't remove later!
        element.addEventListener('click', this.handleClick.bind(this));
    }
    
    destroy() {
        // CAN'T remove the listener! We don't have reference to bound function!
        // element.removeEventListener('click', ???);
    }
}

// ═══════════════════════════════════════════════════════════════
// Better Pattern: Store the bound function reference
// ═══════════════════════════════════════════════════════════════

class GoodExample {
    constructor(element) {
        this.element = element;
        this.boundHandler = this.handleClick.bind(this);  // Store reference
        element.addEventListener('click', this.boundHandler);
    }
    
    destroy() {
        this.element.removeEventListener('click', this.boundHandler);
        this.boundHandler = null;  // Allow garbage collection
    }
}
4

new Binding

new func() - Constructor call β†’ this = newly created object

function Person(name, age) {
    // 1. A new empty object {} is created
    // 2. this = that new object
    // 3. The object's __proto__ is set to Person.prototype
    
    this.name = name;
    this.age = age;
    
    // 4. If no explicit return, 'this' is returned
}

var sandeep = new Person("Sandeep", 25);
console.log(sandeep.name);  // "Sandeep"
console.log(sandeep.age);   // 25

Arrow Functions: NO Own this Binding

Arrow functions don't have their own this. They inherit this from their lexical scope (where they're written).

// ═══════════════════════════════════════════════════════════════
// The Problem: Regular functions in callbacks lose 'this'
// ═══════════════════════════════════════════════════════════════

var person = {
    name: "Sandeep",
    hobbies: ["coding", "reading", "gaming"],
    
    showHobbies: function() {
        this.hobbies.forEach(function(hobby) {
            // 'this' is NOT person here! It's window/undefined
            console.log(this.name + " likes " + hobby);
        });
    }
};

person.showHobbies();
// "undefined likes coding"
// "undefined likes reading"
// "undefined likes gaming"

// ═══════════════════════════════════════════════════════════════
// Solution: Arrow function (inherits this from showHobbies)
// ═══════════════════════════════════════════════════════════════

var person = {
    name: "Sandeep",
    hobbies: ["coding", "reading", "gaming"],
    
    showHobbies: function() {
        // Arrow function has NO own this - uses parent's this
        this.hobbies.forEach((hobby) => {
            console.log(this.name + " likes " + hobby);
        });
    }
};

person.showHobbies();
// "Sandeep likes coding"
// "Sandeep likes reading"
// "Sandeep likes gaming"
// Arrow Functions Cannot be Rebound

var arrowFunc = () => {
    console.log(this);
};

var obj = { name: "Test" };

arrowFunc.call(obj);   // Still logs global/undefined
arrowFunc.apply(obj);  // Still logs global/undefined
arrowFunc.bind(obj)(); // Still logs global/undefined

// Arrow functions IGNORE call/apply/bind for 'this'
// They ALWAYS use lexical this

this Summary Table

BindingHow to Identifythis Value
Defaultfunc()window / undefined (strict)
Implicitobj.func()obj (left of dot)
Explicitcall/apply/bindspecified object
newnew Func()newly created object
Arrow() => {}inherited from lexical scope

🧬 Prototypes & Inheritance

JavaScript uses prototypal inheritance - objects inherit directly from other objects, not from classes.

Prototype Chain Lookup

When you access: obj.property 1. Look in obj itself ↓ Not found? 2. Look in obj.__proto__ (its prototype) ↓ Not found? 3. Look in obj.__proto__.__proto__ ↓ Keep going up the chain... 4. Reach null (end of chain) ↓ Not found? 5. Return undefined

Building a Prototype Chain

var animal = {
    eats: true,
    walk: function() {
        console.log("Walking...");
    }
};

var dog = Object.create(animal);  // dog's prototype = animal
dog.barks = true;
dog.bark = function() {
    console.log("Woof!");
};

var myDog = Object.create(dog);   // myDog's prototype = dog
myDog.name = "Buddy";

// ═══════════════════════════════════════════════════════════════
// Property lookups:
// ═══════════════════════════════════════════════════════════════

console.log(myDog.name);   // "Buddy"      ← Found in myDog itself
console.log(myDog.barks);  // true         ← Found in dog (prototype)
console.log(myDog.eats);   // true         ← Found in animal
myDog.walk();              // "Walking..." ← Found in animal

// Chain visualization:
//
//  myDog = { name: "Buddy" }
//    β”‚
//    └──▢ myDog.__proto__ = dog = { barks: true, bark: fn }
//           β”‚
//           └──▢ dog.__proto__ = animal = { eats: true, walk: fn }
//                  β”‚
//                  └──▢ animal.__proto__ = Object.prototype
//                         β”‚
//                         └──▢ Object.prototype.__proto__ = null

__proto__ vs .prototype

PropertyExists OnPurpose
__proto__Every objectPoints to object's prototype (parent)
.prototypeFunctions onlyBecomes __proto__ of objects created with new
function Person(name) {
    this.name = name;
}

// Person is a function, so it has .prototype
console.log(Person.prototype);  // { constructor: Person }

// Add a method to Person.prototype
Person.prototype.greet = function() {
    console.log("Hello, " + this.name);
};

var sandeep = new Person("Sandeep");

// sandeep's __proto__ points to Person.prototype
console.log(sandeep.__proto__ === Person.prototype);  // true!

// sandeep can use greet() through the prototype chain
sandeep.greet();  // "Hello, Sandeep"

Why Methods Go on Prototype (Memory Efficiency)

// Methods on prototype are SHARED (memory efficient)
function Person(name) {
    this.name = name;
}
Person.prototype.greet = function() { /* ... */ };

var p1 = new Person("Sandeep");
var p2 = new Person("Kumar");

console.log(p1.greet === p2.greet);  // true! Same function

// 1000 people = 1 shared greet function = memory efficient!
// If methods were in constructor:
// 1000 people = 1000 identical functions = wasted memory!

Inheritance with Constructor Functions

// ═══════════════════════════════════════════════════════════════
// Parent constructor
// ═══════════════════════════════════════════════════════════════

function Animal(name) {
    this.name = name;
}

Animal.prototype.speak = function() {
    console.log(this.name + " makes a sound");
};

// ═══════════════════════════════════════════════════════════════
// Child constructor
// ═══════════════════════════════════════════════════════════════

function Dog(name, breed) {
    Animal.call(this, name);  // Call parent constructor with Dog's 'this'
    this.breed = breed;
}

// Set up the prototype chain
Dog.prototype = Object.create(Animal.prototype);

// Fix the constructor reference (it now points to Animal, which is wrong)
Dog.prototype.constructor = Dog;

// Add Dog-specific methods
Dog.prototype.bark = function() {
    console.log(this.name + " says: Woof!");
};

// ═══════════════════════════════════════════════════════════════
// Usage
// ═══════════════════════════════════════════════════════════════

var buddy = new Dog("Buddy", "Golden Retriever");

buddy.bark();   // "Buddy says: Woof!" (own method)
buddy.speak();  // "Buddy makes a sound" (inherited from Animal)

console.log(buddy instanceof Dog);     // true
console.log(buddy instanceof Animal);  // true

Why Dog.prototype.constructor = Dog?

function Animal(name) { this.name = name; }
function Dog(name, breed) {
    Animal.call(this, name);
    this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);

// At this point:
console.log(Dog.prototype.constructor);  // Animal (WRONG!)

// Object.create(Animal.prototype) creates new object
// This new object doesn't have 'constructor' property
// So it looks up the chain and finds Animal.prototype.constructor = Animal

// The fix:
Dog.prototype.constructor = Dog;  // Now correct!

// Why does it matter?
// 1. Debugging - correct constructor shows in console
// 2. Creating new instances from existing ones:
//    var anotherDog = buddy.constructor("Max", "Lab");  // Works correctly now

⚠️ Why Object.setPrototypeOf is Inefficient

var obj = { a: 1 };
var newProto = { b: 2 };

// DON'T DO THIS (slow!)
Object.setPrototypeOf(obj, newProto);

// ═══════════════════════════════════════════════════════════════
// Why is it slow?
// ═══════════════════════════════════════════════════════════════
//
// Modern JavaScript engines (V8, SpiderMonkey) create optimized
// "hidden classes" or "shapes" for objects based on their structure.
//
// When you change an object's prototype AFTER creation:
// 1. Engine has to abandon its optimizations
// 2. Must recalculate property access paths
// 3. Invalidates inline caching
// 4. All code that touches this object becomes slower
//
// It's a "de-optimization" that affects not just this operation,
// but ALL future property accesses on this object!

Why Dog.prototype.constructor = Dog is NOT Slow

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;  // This is FINE!

// ═══════════════════════════════════════════════════════════════
// This is just a regular property assignment!
// ═══════════════════════════════════════════════════════════════
//
// Dog.prototype.constructor = Dog is the same as:
// Dog.prototype.someProperty = someValue
//
// It's NOT changing the prototype chain structure
// It's just adding a property to an existing object
// No chain restructuring, no de-optimization
//
// All just property assignments - completely normal and fast!
Prototypes Best Practices:
  • Methods on prototype - Memory efficient (shared)
  • Data properties in constructor - Each instance gets own copy
  • Don't modify built-in prototypes - Can break other code
  • Set prototype at creation time - Changing later is slow
  • Use Object.create() - Clean prototype setup

πŸ“š ES6 Classes

ES6 Classes are syntactic sugar over the prototype-based inheritance. Under the hood, they work exactly the same way!

Key Insight: ES6 Classes don't introduce a NEW inheritance model. They're just a cleaner way to write constructor functions, prototype methods, and inheritance chains. Same prototype system underneath!

Constructor Function vs Class

// ═══════════════════════════════════════════════════════════════
// OLD WAY: Constructor Function + Prototype
// ═══════════════════════════════════════════════════════════════

function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.greet = function() {
    console.log("Hello, I'm " + this.name);
};

Person.prototype.getAge = function() {
    return this.age;
};

var person1 = new Person("Sandeep", 25);


// ═══════════════════════════════════════════════════════════════
// NEW WAY: ES6 Class (same thing, cleaner syntax!)
// ═══════════════════════════════════════════════════════════════

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    
    greet() {
        console.log("Hello, I'm " + this.name);
    }
    
    getAge() {
        return this.age;
    }
}

const person1 = new Person("Sandeep", 25);


// ═══════════════════════════════════════════════════════════════
// BOTH produce the EXACT same result in memory!
// ═══════════════════════════════════════════════════════════════

Class Syntax Breakdown

class Person {
    // ─────────────────────────────────────────────────────────────
    // CONSTRUCTOR: Called when you do `new Person()`
    // ─────────────────────────────────────────────────────────────
    constructor(name, age) {
        this.name = name;      // Instance property
        this.age = age;        // Instance property
    }
    
    // ─────────────────────────────────────────────────────────────
    // INSTANCE METHODS: Added to Person.prototype (shared!)
    // ─────────────────────────────────────────────────────────────
    greet() {
        console.log("Hello, I'm " + this.name);
    }
    
    // ─────────────────────────────────────────────────────────────
    // GETTER: Access like property (person.info)
    // ─────────────────────────────────────────────────────────────
    get info() {
        return `${this.name}, ${this.age} years old`;
    }
    
    // ─────────────────────────────────────────────────────────────
    // SETTER: Set like property (person.age = 30)
    // ─────────────────────────────────────────────────────────────
    set age(value) {
        if (value < 0) throw new Error("Age can't be negative");
        this._age = value;
    }
    
    get age() {
        return this._age;
    }
    
    // ─────────────────────────────────────────────────────────────
    // STATIC METHOD: Called on class, not instance (Person.create())
    // ─────────────────────────────────────────────────────────────
    static create(name) {
        return new Person(name, 0);
    }
}

const person = new Person("Sandeep", 25);
person.greet();              // "Hello, I'm Sandeep"
console.log(person.info);    // "Sandeep, 25 years old" (getter)

const baby = Person.create("Baby");  // Static method

Class Inheritance (extends & super)

// ═══════════════════════════════════════════════════════════════
// OLD WAY: Prototype chain manually (verbose!)
// ═══════════════════════════════════════════════════════════════

function Animal(name) {
    this.name = name;
}
Animal.prototype.speak = function() {
    console.log(this.name + " makes a sound");
};

function Dog(name, breed) {
    Animal.call(this, name);
    this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
    console.log(this.name + " barks!");
};


// ═══════════════════════════════════════════════════════════════
// NEW WAY: extends & super (so much cleaner!)
// ═══════════════════════════════════════════════════════════════

class Animal {
    constructor(name) {
        this.name = name;
    }
    
    speak() {
        console.log(this.name + " makes a sound");
    }
}

class Dog extends Animal {
    constructor(name, breed) {
        super(name);  // Calls Animal's constructor (REQUIRED!)
        this.breed = breed;
    }
    
    bark() {
        console.log(this.name + " barks!");
    }
    
    // Override parent method
    speak() {
        console.log(this.name + " says: Woof!");
    }
}

const buddy = new Dog("Buddy", "Golden Retriever");
buddy.speak();  // "Buddy says: Woof!" (overridden)
buddy.bark();   // "Buddy barks!"

console.log(buddy instanceof Dog);     // true
console.log(buddy instanceof Animal);  // true

The super Keyword

class Animal {
    constructor(name) {
        this.name = name;
    }
    
    speak() {
        console.log(this.name + " makes a sound");
    }
}

class Dog extends Animal {
    constructor(name, breed) {
        // ─────────────────────────────────────────────────────────
        // super() MUST be called before using `this` in child class
        // ─────────────────────────────────────────────────────────
        super(name);  // Calls parent constructor
        this.breed = breed;
    }
    
    speak() {
        // ─────────────────────────────────────────────────────────
        // super.method() calls parent's method
        // ─────────────────────────────────────────────────────────
        super.speak();  // Call parent's speak
        console.log("...and then barks!");
    }
}

const buddy = new Dog("Buddy", "Lab");
buddy.speak();
// "Buddy makes a sound"
// "...and then barks!"

Private Fields (ES2022)

class BankAccount {
    // ─────────────────────────────────────────────────────────────
    // # makes field PRIVATE (can't access outside class)
    // ─────────────────────────────────────────────────────────────
    #balance;
    #pin;
    
    constructor(initialBalance, pin) {
        this.#balance = initialBalance;
        this.#pin = pin;
    }
    
    deposit(amount) {
        this.#balance += amount;
        console.log("Deposited:", amount);
    }
    
    withdraw(amount, pin) {
        if (pin !== this.#pin) {
            console.log("Wrong PIN!");
            return;
        }
        if (amount > this.#balance) {
            console.log("Insufficient funds!");
            return;
        }
        this.#balance -= amount;
        console.log("Withdrew:", amount);
    }
    
    getBalance(pin) {
        if (pin !== this.#pin) return "Wrong PIN!";
        return this.#balance;
    }
}

const account = new BankAccount(1000, "1234");

account.deposit(500);
console.log(account.getBalance("1234"));  // 1500

// Trying to access private fields:
console.log(account.#balance);  // SyntaxError: Private field!
console.log(account.balance);   // undefined (not the same)

Static Members

class MathUtils {
    // ─────────────────────────────────────────────────────────────
    // Static properties & methods belong to CLASS, not instances
    // ─────────────────────────────────────────────────────────────
    static PI = 3.14159;
    
    static square(x) {
        return x * x;
    }
    
    static cube(x) {
        return x * x * x;
    }
}

// Called on CLASS, not instance
console.log(MathUtils.PI);         // 3.14159
console.log(MathUtils.square(5));  // 25
console.log(MathUtils.cube(3));    // 27

// Can't call on instance
const math = new MathUtils();
console.log(math.PI);        // undefined
console.log(math.square(5)); // TypeError: not a function


// ═══════════════════════════════════════════════════════════════
// Common use case: Factory methods, counters, utilities
// ═══════════════════════════════════════════════════════════════

class User {
    static #count = 0;  // Private static
    
    constructor(name) {
        this.name = name;
        this.id = ++User.#count;  // Auto-increment ID
    }
    
    static getCount() {
        return User.#count;
    }
}

const user1 = new User("Sandeep");  // id: 1
const user2 = new User("Kumar");    // id: 2
console.log(User.getCount());       // 2

Class vs Function Differences

FeatureClassConstructor Function
Hoisting❌ Not hoisted (TDZ)βœ… Fully hoisted
Strict ModeAlways strictDepends on context
Must use newβœ… Required❌ Optional (but should)
Methods enumerable❌ Non-enumerableβœ… Enumerable by default
typeof"function""function"
// ═══════════════════════════════════════════════════════════════
// 1. Classes are NOT hoisted like function declarations
// ═══════════════════════════════════════════════════════════════

const p = new Person("Test");  // ReferenceError!
class Person { }

// Functions are hoisted:
const p = new PersonFunc("Test");  // Works!
function PersonFunc(name) { this.name = name; }


// ═══════════════════════════════════════════════════════════════
// 2. Classes ALWAYS run in strict mode
// ═══════════════════════════════════════════════════════════════

class StrictClass {
    method() {
        console.log(this);  // undefined if called without context
    }
}

const obj = new StrictClass();
const method = obj.method;
method();  // undefined (not window!)


// ═══════════════════════════════════════════════════════════════
// 3. Must use 'new' with classes
// ═══════════════════════════════════════════════════════════════

class Person {
    constructor(name) { this.name = name; }
}

Person("Sandeep");      // TypeError: Cannot call without 'new'
new Person("Sandeep");  // Works!

Proof That Classes Are Just Functions

class Person {
    constructor(name) {
        this.name = name;
    }
    greet() {
        console.log("Hello!");
    }
}

// Proof that class is just a function:
console.log(typeof Person);  // "function"

// Proof that methods are on prototype:
console.log(Person.prototype.greet);  // function greet()

// Proof that instances work the same:
const p = new Person("Sandeep");
console.log(p.__proto__ === Person.prototype);  // true

// You can even add to the prototype:
Person.prototype.wave = function() {
    console.log(this.name + " waves!");
};
p.wave();  // "Sandeep waves!"

ES6 Classes Cheat Sheet

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ ES6 CLASSES CHEAT SHEET β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ class Person { β”‚ β”‚ #privateField; // Private (ES2022) β”‚ β”‚ publicField = value; // Public field β”‚ β”‚ β”‚ β”‚ constructor() { } // Called with 'new' β”‚ β”‚ β”‚ β”‚ method() { } // Instance method (on prototype) β”‚ β”‚ get prop() { } // Getter β”‚ β”‚ set prop(v) { } // Setter β”‚ β”‚ β”‚ β”‚ static method() { } // Class method (not on instance) β”‚ β”‚ static field = v; // Class property β”‚ β”‚ } β”‚ β”‚ β”‚ β”‚ class Child extends Parent { β”‚ β”‚ constructor() { β”‚ β”‚ super(); // MUST call before using 'this' β”‚ β”‚ } β”‚ β”‚ method() { β”‚ β”‚ super.method(); // Call parent method β”‚ β”‚ } β”‚ β”‚ } β”‚ β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ KEY DIFFERENCES FROM FUNCTIONS: β”‚ β”‚ β€’ Not hoisted (TDZ applies) β”‚ β”‚ β€’ Always strict mode β”‚ β”‚ β€’ Must use 'new' β”‚ β”‚ β€’ Methods are non-enumerable β”‚ β”‚ β”‚ β”‚ UNDER THE HOOD: β”‚ β”‚ β€’ Still prototype-based β”‚ β”‚ β€’ typeof class === "function" β”‚ β”‚ β€’ Methods still on .prototype β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜