Master the most important JavaScript concepts: closures, hoisting, prototype, event loop, scope, async/await, coercion, and ES6+. Deeply explained with examples and output.
These are the foundational pillars of JavaScript mastery. Understanding them deeply — not just surface-level — is what separates average developers from great ones.
2. Closure — The Most Important Concept
A closure is the combination of a function and its lexical environment (the surrounding scope at the time of creation).
// Every function in JavaScript IS a closure
function outer(greeting) {
// greeting is in closure for inner
return function inner(name) {
return `${greeting}, ${name}!`;
};
}
const hello = outer("Hello");
const namaste = outer("Namaste");
console.log(hello("Alice")); // Hello, Alice!
console.log(namaste("Bob")); // Namaste, Bob!
console.log(hello("Charlie")); // Hello, Charlie!
Hello, Alice!
Namaste, Bob!
Hello, Charlie!
Why this matters:
outer has returned and its context is gone from the call stack- But
inner still has access to greeting through the closure - Each call to
outer creates a new closure with separate state
Closure Use Cases
| Use Case | Pattern |
|---|
| Private state | Counter, BankAccount |
| Factory functions | createMultiplier(n) |
| Memoization | cache results by args |
| Debounce/Throttle | delay/limit execution |
| Module pattern | IIFE with public API |
| Partial application | Fix some arguments |
3. Prototype-Based Inheritance
JavaScript is prototype-based, not class-based. Every object has an internal [[Prototype]] link.
const animal = {
breathe() { return `${this.name} breathes`; }
};
const dog = Object.create(animal); // dog's [[Prototype]] = animal
dog.name = "Rex";
dog.bark = function() { return `${this.name} barks!`; };
console.log(dog.breathe()); // Rex breathes — from prototype!
console.log(dog.bark()); // Rex barks!
console.log(dog.hasOwnProperty("name")); // true
console.log(dog.hasOwnProperty("breathe")); // false — on prototype
Rex breathes
Rex barks!
true
false
Prototype Chain for dog:
dog → animal → Object.prototype → null
Constructor Function Pattern
function Person(name, age) {
this.name = name; // own property
this.age = age; // own property
}
// Shared method — one copy in memory
Person.prototype.greet = function() {
return `Hi, I'm ${this.name}`;
};
const alice = new Person("Alice", 25);
const bob = new Person("Bob", 30);
// Both share the SAME greet function
console.log(alice.greet === bob.greet); // true!
4. this Keyword — Context Rules
this is not fixed — it depends on how a function is called.
// Rule 1: Default binding (standalone call)
function fn() { console.log(this); }
fn(); // window (non-strict) or undefined (strict)
// Rule 2: Implicit binding (method call)
const obj = { name: "JS", fn() { console.log(this.name); } };
obj.fn(); // "JS"
// Rule 3: Explicit binding
fn.call({ name: "explicit" }); // { name: "explicit" }
fn.apply({ name: "apply" }); // { name: "apply" }
const bound = fn.bind({ name: "bound" });
bound(); // { name: "bound" }
// Rule 4: new binding
function Car(model) { this.model = model; }
const car = new Car("Tesla");
console.log(car.model); // "Tesla"
// Arrow function — no own 'this'
const obj2 = {
name: "Arrow",
fn: () => console.log(this?.name) // lexical 'this'
};
Priority: new > explicit (call/apply/bind) > method call > default
5. Event Loop & Async JavaScript
JavaScript is single-threaded but handles async through the event loop.
┌──────────────┐
│ Call Stack │ ← synchronous execution
└──────┬───────┘
↑ pop tasks when empty
┌──────┴────────────────┐ ┌──────────────────────────┐
│ Microtask Queue │ │ Macrotask Queue │
│ (Promise.then, await) │ │ (setTimeout, setInterval) │
│ ← Drained ENTIRELY │ │ ← ONE at a time │
│ before macrotask │ │ │
└───────────────────────┘ └────────────────────────────┘
console.log("sync 1");
setTimeout(() => console.log("macro 1"), 0);
setTimeout(() => console.log("macro 2"), 0);
Promise.resolve().then(() => console.log("micro 1"));
Promise.resolve().then(() => console.log("micro 2"));
console.log("sync 2");
sync 1
sync 2
micro 1
micro 2
macro 1
macro 2
6. Type Coercion — The Hidden Rules
JavaScript automatically converts types in many situations.
Implicit Coercion Rules
// String context (template literals, + with string)
"" + 1 // "1"
"" + true // "true"
"" + null // "null"
"" + [] // ""
"" + {} // "[object Object]"
// Numeric context (-, *, /, ==, <, >)
"5" - 1 // 4
"5" * 2 // 10
true + true // 2
null + 1 // 1 (null → 0)
undefined + 1 // NaN (undefined → NaN)
// Boolean context (if, !!, ternary, &&, ||)
Boolean(0) // false
Boolean("") // false
Boolean(null) // false
Boolean(undefined) // false
Boolean(NaN) // false
Boolean([]) // TRUE (empty array is truthy!)
Boolean({}) // TRUE (empty object is truthy!)
"1"
4
10
2
1
NaN
false
true
true
7. Higher-Order Functions
Functions that take functions as arguments or return functions.
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// map: transform each element
const squared = numbers.map(n => n ** 2);
// [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
// filter: keep matching elements
const evens = numbers.filter(n => n % 2 === 0);
// [2, 4, 6, 8, 10]
// reduce: accumulate to single value
const sum = numbers.reduce((acc, n) => acc + n, 0);
// 55
// Chain them
const result = numbers
.filter(n => n % 2 === 0) // [2, 4, 6, 8, 10]
.map(n => n ** 2) // [4, 16, 36, 64, 100]
.reduce((acc, n) => acc + n, 0); // 220
console.log(result);
8. Currying & Partial Application
Currying: Transform f(a, b, c) into f(a)(b)(c).
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) return fn(...args);
return (...more) => curried(...args, ...more);
};
}
const add = (a, b, c) => a + b + c;
const cAdd = curry(add);
console.log(cAdd(1)(2)(3)); // 6
console.log(cAdd(1, 2)(3)); // 6
// Real-world: configurable validators
const validate = curry((rule, errorMsg, value) => {
return rule(value) ? null : errorMsg;
});
const required = validate(v => v != null && v !== "", "Field is required");
const minLen5 = validate(v => v.length >= 5, "Min 5 chars");
console.log(required("")); // "Field is required"
console.log(required("hello")); // null
console.log(minLen5("hi")); // "Min 5 chars"
6
6
Field is required
null
Min 5 chars
9. Memoization
Cache expensive function results to avoid recomputation.
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key); // ← from cache
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// Without memoization: O(2^n)
const slowFib = n => n <= 1 ? n : slowFib(n-1) + slowFib(n-2);
// With memoization: O(n)
const fib = memoize(n => n <= 1 ? n : fib(n-1) + fib(n-2));
console.log(fib(40)); // fast!
console.log(fib(40)); // instant — from cache
10. Immutability & Pure Functions
Pure function: Same input → same output, no side effects.
// ❌ Impure: modifies external state
let total = 0;
function addToTotal(n) {
total += n; // side effect!
return total;
}
// ✅ Pure: no side effects
function add(a, b) { return a + b; } // always returns a + b
// ❌ Impure array operation (mutates)
function addItem(arr, item) {
arr.push(item); // modifies original!
return arr;
}
// ✅ Pure array operation
function addItemPure(arr, item) {
return [...arr, item]; // returns new array
}
const arr1 = [1, 2, 3];
const arr2 = addItemPure(arr1, 4);
console.log(arr1); // [1, 2, 3] — unchanged!
console.log(arr2); // [1, 2, 3, 4]
11. Generators
Functions that can pause and resume execution.
function* counter(start = 0) {
while (true) {
const reset = yield start;
start = reset !== undefined ? reset : start + 1;
}
}
const gen = counter(10);
console.log(gen.next().value); // 10
console.log(gen.next().value); // 11
console.log(gen.next(0).value); // 0 (reset!)
console.log(gen.next().value); // 1
// Infinite range
function* range(start, end, step = 1) {
for (let i = start; i <= end; i += step) {
yield i;
}
}
console.log([...range(0, 10, 2)]); // [0, 2, 4, 6, 8, 10]
10
11
0
1
[0, 2, 4, 6, 8, 10]
12. Proxy & Reflect
Proxy intercepts operations on objects.
function createValidatedObject(target, rules) {
return new Proxy(target, {
set(obj, prop, value) {
if (rules[prop] && !rules[prop](value)) {
throw new TypeError(`Invalid value for ${prop}: ${value}`);
}
obj[prop] = value;
return true;
}
});
}
const user = createValidatedObject({}, {
age: v => typeof v === "number" && v >= 0 && v <= 120,
name: v => typeof v === "string" && v.length > 0
});
user.name = "Alice"; // OK
user.age = 25; // OK
console.log(user); // { name: "Alice", age: 25 }
try {
user.age = -1; // Throws!
} catch (e) {
console.log(e.message);
}
{ name: "Alice", age: 25 }
Invalid value for age: -1
13. Memory Management & Garbage Collection
JavaScript uses mark-and-sweep garbage collection.
REACHABLE objects are kept; UNREACHABLE objects are collected.
Reachable = accessible from roots (global, call stack, closures)
Common memory leak patterns:
// 1. Forgotten timers
const data = { huge: new Array(1000000) };
setInterval(() => {
// data is never collected while timer runs!
console.log(data.huge.length);
}, 1000);
// Fix: clearInterval when done
// 2. Closure over large data
function bad() {
const huge = new Array(1000000).fill("x");
return function() { console.log("done"); };
// huge is captured even though not used!
}
function good() {
const huge = new Array(1000000).fill("x");
const len = huge.length; // capture only what's needed
return function() { console.log(len); };
}
// 3. Detached DOM nodes
let div = document.getElementById("modal");
div.remove();
// div variable still holds reference → not GC'd!
div = null; // Fix: nullify the reference
Common Traps Summary 🪤
| Concept | Common Mistake | Correct Understanding |
|---|
this in callback | Loses context | Use arrow fn or .bind() |
| Closure loop | var shared | Use let or IIFE |
typeof null | "null" | "object" (JS bug) |
NaN check | NaN === NaN | Number.isNaN(x) |
| Prototype modify | Modifying Object.prototype | Extend constructor prototype |
| Promise.all | Doesn't handle failures | Use .allSettled for resilience |
async forEach | Doesn't await | Use for...of with await |