JavaScript Advanced Course
Master the hardest parts of JavaScript with 25 advanced lessons. Deep-dive into closures internals, prototypes, generators, proxies, the event loop, Web Workers, design patterns, and performance — each with code, output, and official MDN docs.
Start LearningExecution Context & Call Stack
Every time a function is called, JavaScript creates an execution context that holds its variables, arguments, and the value of this. These contexts are pushed onto a call stack. When a function returns, its context is popped off. The call stack is why JavaScript is called single-threaded.
function first() {
console.log("first start");
second();
console.log("first end");
}
function second() {
console.log("second");
}
first();
Lexical Environment & Scope Chain
A lexical environment is the hidden structure that stores variables in a scope, along with a reference to its outer environment. When you access a variable, JavaScript walks this chain from inner to outer until it finds the variable or reaches the global scope.
let global = "global";
function outer() {
let outerVar = "outer";
function inner() {
let innerVar = "inner";
console.log(innerVar, outerVar, global);
}
inner();
}
outer();
`this` Binding Rules Deep Dive
The value of this is determined by four rules, in priority order: 1) new binding (constructor calls), 2) explicit binding (call, apply, bind), 3) implicit binding (method call), 4) default binding (global or undefined in strict mode). Arrow functions ignore these rules and inherit this from their surrounding scope.
const obj = {
name: "Implicit",
show() { return this.name; }
};
function show() { return this?.name ?? "Default"; }
console.log(obj.show());
console.log(show.call({ name: "Explicit" }));
console.log(show());
Prototypal Inheritance Patterns
JavaScript uses prototypes, not classical inheritance. Objects can inherit directly from other objects. The two main patterns are the factory pattern (Object.create) and the constructor/prototype pattern. Understanding the prototype chain is key to mastering JavaScript's object model.
const vehicle = {
start() { return "Starting..."; }
};
const car = Object.create(vehicle);
car.drive = function() { return "Driving"; };
console.log(car.drive());
console.log(car.start());
console.log(Object.getPrototypeOf(car) === vehicle);
Class Inheritance & Mixins
extends gives single inheritance: one class inherits from one parent. Mixins let you combine behavior from multiple sources by copying methods from several objects into a class prototype. Mixins use Object.assign to merge capabilities.
const canFly = {
fly() { return "Flying"; }
};
const canSwim = {
swim() { return "Swimming"; }
};
class Duck {}
Object.assign(Duck.prototype, canFly, canSwim);
const duck = new Duck();
console.log(duck.fly(), duck.swim());
ES6 Private Class Fields (#)
Private class fields use the # prefix and are truly private — they cannot be accessed from outside the class, not even by subclasses. This gives real encapsulation, unlike the old convention of underscore-prefixed pseudo-private properties.
class BankAccount {
#balance = 0;
deposit(amount) {
this.#balance += amount;
return this.#balance;
}
getBalance() {
return this.#balance;
}
}
const acc = new BankAccount();
acc.deposit(100);
console.log(acc.getBalance());
console.log(acc.#balance);
Symbols
Symbol is a primitive type used to create unique identifiers. Every Symbol() call returns a value guaranteed to be unique. Symbols are often used as object keys to avoid name collisions, and well-known symbols like Symbol.iterator customize object behavior.
const id1 = Symbol("id");
const id2 = Symbol("id");
const user = {
[id1]: 123
};
console.log(id1 === id2);
console.log(user[id1]);
Iterators & Iterables
An iterable is any object that defines a Symbol.iterator method returning an iterator. An iterator is an object with a next() method that returns { value, done }. Arrays, strings, maps, and sets are built-in iterables, which is why for...of works on them.
const range = {
from: 1,
to: 3,
[Symbol.iterator]() {
let current = this.from;
return {
next: () => {
if (current <= this.to) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
}
};
}
};
for (const n of range) {
console.log(n);
}
Generators (function*)
A generator is a function that can pause and resume execution. It is declared with function* and uses the yield keyword to return values one at a time. Calling a generator returns an iterator, and each .next() call resumes execution until the next yield.
function* countUp() {
yield 1;
yield 2;
yield 3;
}
const gen = countUp();
console.log(gen.next());
console.log(gen.next());
console.log(gen.next());
console.log(gen.next());
Generator Use Cases (Lazy Evaluation)
Generators shine when you want lazy evaluation — producing values only when asked, instead of computing everything upfront. This is ideal for infinite sequences, large data streams, and custom control flow.
function* fibonacci() {
let a = 0, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const fib = fibonacci();
for (let i = 0; i < 6; i++) {
console.log(fib.next().value);
}
Proxies
A Proxy wraps an object and intercepts operations on it, like reading, writing, and deleting properties. A trap (handler function) runs instead of the default operation. Proxies enable validation, logging, and reactive programming.
const target = { name: "Ali", age: 25 };
const handler = {
get(obj, prop) {
console.log(`Reading ${prop}`);
return obj[prop];
},
set(obj, prop, value) {
if (prop === "age" && value < 0) {
throw new Error("Age cannot be negative");
}
obj[prop] = value;
return true;
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.name);
proxy.age = -5;
Reflect API
Reflect provides methods that mirror JavaScript's internal operations — like Reflect.get, Reflect.set, and Reflect.has — as normal functions. It is often used inside Proxy traps to perform the default behavior cleanly.
const obj = { name: "Ali" };
console.log(Reflect.get(obj, "name"));
console.log(Reflect.has(obj, "age"));
Reflect.set(obj, "age", 25);
console.log(Reflect.get(obj, "age"));
WeakMap & WeakSet
WeakMap and WeakSet are like Map and Set, but they hold weak references to their keys. If an object used as a key has no other references, it can be garbage collected. This prevents memory leaks when tracking objects.
let user = { name: "Ali" };
const visits = new WeakMap();
visits.set(user, 5);
console.log(visits.get(user));
user = null;
// The entry is now eligible for garbage collection
Memory Management & Garbage Collection
JavaScript automatically manages memory using garbage collection. The main algorithm is mark-and-sweep: objects that are reachable from roots (global object, call stack) are kept; unreachable objects are freed. You can help by removing references when data is no longer needed.
function createData() {
let big = { data: new Array(1000).fill("x") };
return big;
}
let holder = createData();
console.log(holder.data.length);
holder = null; // release the reference
Event Loop Deep Dive (Sync → Micro → Macro)
The event loop processes tasks in a precise order: all synchronous code runs first, then all microtasks (Promises, queueMicrotask) are drained, then one macrotask (setTimeout, I/O) runs, and the cycle repeats. Understanding this order prevents subtle bugs.
console.log("1: sync");
setTimeout(() => console.log("4: macrotask"), 0);
Promise.resolve().then(() => {
console.log("2: microtask");
queueMicrotask(() => console.log("3: nested microtask"));
});
console.log("5: sync end");
Microtasks & queueMicrotask
Microtasks are a special queue that runs immediately after the current synchronous code, before any macrotask. Promise callbacks and queueMicrotask both schedule microtasks. queueMicrotask lets you explicitly schedule one without creating a Promise.
console.log("start");
queueMicrotask(() => console.log("microtask"));
setTimeout(() => console.log("macrotask"), 0);
console.log("end");
Promise Combinators (all, allSettled, race, any)
Promise has four static combinators. Promise.all resolves when all succeed or rejects on the first error. Promise.allSettled waits for all and reports every outcome. Promise.race settles with the first to settle. Promise.any resolves with the first success.
const p1 = Promise.resolve(10);
const p2 = new Promise(res => setTimeout(() => res(20), 100));
const p3 = Promise.reject("fail");
Promise.all([p1, p2]).then(console.log);
Promise.allSettled([p1, p3]).then(console.log);
Promise.race([p1, p2]).then(console.log);
Promise.any([p3, p2]).then(console.log);
AbortController & Fetch Cancellation
AbortController lets you cancel an in-flight fetch request. You create a controller, pass its signal to fetch, and call controller.abort() to cancel. The fetch then rejects with an AbortError, which you can handle gracefully.
const controller = new AbortController();
setTimeout(() => controller.abort(), 100);
fetch("https://jsonplaceholder.typicode.com/todos/1", {
signal: controller.signal
})
.then(res => res.json())
.then(data => console.log(data))
.catch(err => {
if (err.name === "AbortError") {
console.log("Request cancelled");
}
});
Web Workers
Web Workers run JavaScript in a separate thread, keeping the main thread responsive. They communicate with the main thread using postMessage and onmessage. Workers are ideal for heavy computations that would otherwise freeze the UI.
// main.js
const worker = new Worker("worker.js");
worker.postMessage({ type: "sum", n: 1000000 });
worker.onmessage = (e) => {
console.log("Result:", e.data);
};
// worker.js
self.onmessage = (e) => {
let sum = 0;
for (let i = 1; i <= e.data.n; i++) {
sum += i;
}
self.postMessage(sum);
};
Design Patterns (Module, Singleton, Observer, Factory)
Design patterns are reusable solutions to common problems. The Module pattern uses closures for encapsulation. Singleton ensures one instance. Observer lets objects subscribe to events. Factory centralizes object creation.
const singleton = (() => {
let instance;
function create() {
return { name: "Only One" };
}
return {
getInstance() {
if (!instance) instance = create();
return instance;
}
};
})();
const a = singleton.getInstance();
const b = singleton.getInstance();
console.log(a === b, a.name);
Recursion & Performance
Recursion is when a function calls itself. It is elegant for problems like factorials and tree traversal, but deep recursion can hit the call stack limit. Tail recursion and iterative alternatives can reduce stack pressure.
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
console.log(factorial(5));
Currying & Partial Application
Currying transforms a function with multiple arguments into a chain of single-argument functions. Partial application fixes some arguments upfront and returns a function expecting the rest. Both create specialized functions from generic ones.
function multiply(a) {
return function(b) {
return function(c) {
return a * b * c;
};
};
}
const double = multiply(2);
const doubleThenTriple = double(3);
console.log(doubleThenTriple(4));
Memoization
Memoization caches the results of expensive function calls keyed by their arguments. When the same inputs occur again, the cached result is returned instantly instead of recomputing. This is a major performance optimization for pure functions.
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log("from cache");
return cache.get(key);
}
const result = fn(...args);
cache.set(key, result);
console.log("computed");
return result;
};
}
function slowAdd(a, b) {
return a + b;
}
const fastAdd = memoize(slowAdd);
fastAdd(2, 3);
fastAdd(2, 3);
RegExp Deep Dive
Regular expressions are patterns for matching text. The RegExp object and regex literals let you test, search, and replace strings. Common flags are g (global), i (case-insensitive), and m (multiline). Character classes and quantifiers build powerful patterns.
const email = "[email protected]"; const pattern = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i; console.log(pattern.test(email)); console.log(email.match(/[0-9]+/g));
Advanced Recap + Capstone
You now know execution contexts, scope chains, this binding, prototypal inheritance, classes, private fields, symbols, iterators, generators, proxies, Reflect, WeakMap/WeakSet, memory management, the event loop, microtasks, promise combinators, AbortController, Web Workers, design patterns, recursion, currying, memoization, and regex. Apply them in a capstone that combines several advanced features.
class Store {
#data = {};
constructor() {
return new Proxy(this, {
get(target, prop) {
if (prop in target.#data) return target.#data[prop];
return target[prop];
},
set(target, prop, value) {
if (prop === "data") return false;
target.#data[prop] = value;
return true;
}
});
}
all() {
return { ...this.#data };
}
}
const store = new Store();
store.name = "Ali";
console.log(store.name);
console.log(store.all());
JavaScript Advanced Projects
Apply debouncing, cancellation, and the IntersectionObserver API. Click each project to open its full guide.
About this project
Build a search box that calls an API as the user types — but only after they stop typing (debounce), and it cancels stale requests when a new search starts (AbortController).
What you'll practice
Step-by-step: How it works
- The user types into the search input.
- A debounced function waits 300ms of inactivity before running.
- Each new search creates an AbortController.
- Before starting a new fetch, the previous request is aborted.
- The latest successful response is printed.
- Aborted requests are silently ignored, not treated as errors.
function debounce(fn, delay = 300) {
let timer;
let controller;
return function(query) {
clearTimeout(timer);
if (controller) controller.abort();
timer = setTimeout(async () => {
controller = new AbortController();
try {
const response = await fetch(
`https://api.example.com/search?q=${query}`,
{ signal: controller.signal }
);
const data = await response.json();
console.log("Results:", data);
} catch (err) {
if (err.name === "AbortError") {
console.log("Cancelled old search");
} else {
console.log("Error:", err.message);
}
}
}, delay);
};
}
const search = debounce(handleSearch);
search("ja");
search("jav");
search("java");
About this project
Load more content automatically when the user scrolls to a sentinel element. IntersectionObserver watches the sentinel and triggers a fetch for the next page when it becomes visible.
What you'll practice
Step-by-step: How it works
- A sentinel div is placed at the bottom of the list.
- IntersectionObserver watches that sentinel.
- When the sentinel becomes visible, the next page is fetched.
- New items are appended to the list.
- The observer keeps watching until no more data remains.
let page = 1;
let loading = false;
const sentinel = document.getElementById("sentinel");
const list = document.getElementById("list");
const observer = new IntersectionObserver(async (entries) => {
const entry = entries[0];
if (entry.isIntersecting && !loading) {
loading = true;
const response = await fetch(`/api/posts?page=${page}`);
const posts = await response.json();
if (posts.length === 0) {
observer.disconnect();
} else {
posts.forEach(post => {
const div = document.createElement("div");
div.textContent = post.title;
list.appendChild(div);
});
page++;
}
loading = false;
}
});
observer.observe(sentinel);
Advanced Capstone Project
Combine proxies, private fields, localStorage, and reactive patterns into one complete project.
About this project
Build a reactive store that persists to localStorage and notifies subscribers whenever data changes. It uses a Proxy to intercept writes, a private field to hold state, and a simple pub/sub system.
What you'll practice
Step-by-step: How it works
- The Store class keeps state in a private #data object.
- A Proxy intercepts property writes.
- Each write updates #data, persists to localStorage, and notifies subscribers.
- Subscribers register with subscribe() and receive the key and new value on every change.
- The store loads any saved state from localStorage in its constructor.
class Store {
#data = {};
#subscribers = [];
constructor(key = "store") {
this.key = key;
const saved = localStorage.getItem(key);
if (saved) this.#data = JSON.parse(saved);
return new Proxy(this, {
set(target, prop, value) {
if (prop === "key" || prop === "data") return false;
target.#data[prop] = value;
target.#save();
target.#notify(prop, value);
return true;
},
get(target, prop) {
if (prop in target.#data) return target.#data[prop];
return target[prop];
}
});
}
#save() {
localStorage.setItem(this.key, JSON.stringify(this.#data));
}
#notify(key, value) {
this.#subscribers.forEach(fn => fn(key, value));
}
subscribe(fn) {
this.#subscribers.push(fn);
}
all() {
return { ...this.#data };
}
}
const store = new Store("app-state");
store.subscribe((key, value) => {
console.log(`Changed: ${key} = ${value}`);
});
store.theme = "dark";
store.language = "en";
console.log(store.all());