π¦ 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?
Types of Execution Context
| Type | Description | When Created |
|---|---|---|
| Global | Created when JS file starts, only ONE per program | Script starts |
| Function | Created every time a function is called | Function invoked |
| Eval | Created 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 toundefinedlet/constβ uninitialized (TDZ)- Function declarations β stored entirely in memory
thisbinding 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
- 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.
Hoisting Behavior by Type
| Declaration | Hoisted? | Initial Value | Accessible Before? |
|---|---|---|---|
var | β Yes | undefined | β Yes (returns undefined) |
let | β Yes | Uninitialized | β TDZ Error |
const | β Yes | Uninitialized | β TDZ Error |
function declaration | β Yes | Entire function | β Fully works! |
function expression | As variable | Same as var/let/const | Same as variable type |
class declaration | β Yes | Uninitialized | β 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.
// 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.
Scope Chain Lookup
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!"
- 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
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.
- Lexical Scope (closures): WHERE function is WRITTEN
- this Binding: HOW function is CALLED
The 4 Rules of this Binding (Priority Order)
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
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
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
- 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/applyhave 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
}
}
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
| Binding | How to Identify | this Value |
|---|---|---|
| Default | func() | window / undefined (strict) |
| Implicit | obj.func() | obj (left of dot) |
| Explicit | call/apply/bind | specified object |
| new | new 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
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
| Property | Exists On | Purpose |
|---|---|---|
__proto__ | Every object | Points to object's prototype (parent) |
.prototype | Functions only | Becomes __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!
- 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!
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
| Feature | Class | Constructor Function |
|---|---|---|
| Hoisting | β Not hoisted (TDZ) | β Fully hoisted |
| Strict Mode | Always strict | Depends 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!"