Part 1 — Understanding this First (The Foundation)
Why does this exist?
JavaScript functions are not attached to any object by default. Unlike Java or C++ where methods always belong to a class, a JS function is a standalone value — you can assign it to any variable, pass it anywhere, call it from any context. This creates a problem: how does a function know which object’s data to work with?
That’s what this solves. It’s a dynamic reference that tells the
function: “here’s the object you’re working on right now.” The same
function can work with different objects just by changing this.
The 5 Rules of this — In Priority Order
When JS determines this, it checks these rules top to bottom. The
first match wins:
newkeyword —new Foo()→this= a brand new empty object. Why: the constructor needs a fresh object to set up.- Explicit binding —
fn.call(obj)/fn.apply(obj)/fn.bind(obj)→this=obj. Why: you’re explicitly telling JS “use THIS object.” It overrides the default behavior. - Implicit binding —
obj.fn()→this=obj(the object before the dot). Why: when you call a method on an object, JS assumes you want that object’s data. - Default binding —
fn()→this=window(non-strict) orundefined(strict). Why: no object context, so JS falls back to the global object. Strict mode makes this safer by using undefined. - Arrow function — no own
this, inherits from enclosing scope. Why: arrow functions were created specifically to solve the “lost this” problem in callbacks and nested functions.
The “Lost this” Problem — Why call/apply/bind Were Created
const user = {
name: "Akshay",
greet: function() {
console.log("Hello, " + this.name);
}
};
user.greet(); // "Hello, Akshay" ✓ — called as method, this = user
// Now extract the function and pass it somewhere else:
const fn = user.greet;
fn(); // "Hello, undefined" ✗ — standalone call, this = window!
// Same problem with setTimeout:
setTimeout(user.greet, 1000); // "Hello, undefined" ✗call() — “Call this function right now, with this context”
What it does & why it exists
fn.call(context, arg1, arg2) — calls the function immediately with
this set to context, and passes arguments one by one.
Why it exists: Sometimes you have a function that uses this, and
you want to run it with a specific object as this — right now, not
later. Maybe you’re borrowing a method from one object for another.
Maybe you’re calling a parent constructor. call is the direct way to
say “execute this function, and when it looks at this, it should see
this object.”
Example 1: Method Borrowing — The #1 Interview Use Case
const person1 = {
name: "Akshay",
printDetails: function(city, country) {
console.log(this.name + " from " + city + ", " + country);
}
};
const person2 = { name: "Alok" };
const person3 = { name: "Pranav" };
// person2 and person3 don't have printDetails, but we BORROW it:
person1.printDetails.call(person2, "Delhi", "India");
// "Alok from Delhi, India"
person1.printDetails.call(person3, "Pune", "India");
// "Pranav from Pune, India"Example 2: Calling Parent Constructor — Inheritance Pattern
function Animal(name, sound) {
this.name = name;
this.sound = sound;
}
function Dog(name) {
Animal.call(this, name, "Woof");
// ^ We're saying: "Run the Animal constructor,
// but set 'this' to the new Dog object."
// This gives Dog all of Animal's properties.
this.type = "Dog";
}
const buddy = new Dog("Buddy");
console.log(buddy);
// { name: "Buddy", sound: "Woof", type: "Dog" }Example 3: Borrowing Array Methods for Array-Like Objects
function listArgs() {
// 'arguments' looks like an array but ISN'T one.
// It has .length and indexes but NO .map, .filter, .forEach
// Borrow Array's slice to convert it to a real array:
const realArray = Array.prototype.slice.call(arguments);
console.log(realArray);
}
listArgs(1, 2, 3); // [1, 2, 3] — a real array now!
// Modern equivalents (ES6+):
// Array.from(arguments)
// [...arguments]
// Or just use rest params: function listArgs(...args) {}apply() — Same as call, but arguments in an array
Why does apply exist if we already have call?
fn.apply(context, [arg1, arg2]) — identical to call except arguments
are passed as an array.
Why it exists: Sometimes your arguments are already in an array.
Before ES6’s spread operator (...), there was NO way to “unpack” an
array into individual arguments. apply was invented to solve this.
Today, ...spread does the same thing, so apply is less commonly
needed — but interviewers still test it.
The Classic Use Case: Math.max with an Array
const numbers = [5, 2, 8, 1, 9];
// Math.max expects individual args: Math.max(5, 2, 8, 1, 9)
// But we have an array! We can't do Math.max([5,2,8,1,9]) — that gives NaN.
// BEFORE ES6 — apply was the ONLY solution:
Math.max.apply(null, numbers); // 9
// null because Math.max doesn't use 'this' — we don't care about context
// AFTER ES6 — spread operator replaced this pattern:
Math.max(...numbers); // 9 — cleaner and easier to readbind() — “Give me a copy with this locked forever”
Why bind is fundamentally different from call/apply
const newFn = fn.bind(context, arg1, arg2) — does NOT call the
function. Returns a new function with this permanently set.
Why it exists: call and apply are for immediate execution.
But what if you need to fix this for later? When you pass a method
as a callback to setTimeout, addEventListener, or Array.map, you
can’t use call because you’re not calling the function — you’re
giving it to someone else to call later. bind creates a version of
the function where this is baked in permanently.
The Problem bind Solves — setTimeout losing this
const user = {
name: "Akshay",
greet: function() {
console.log("Hello, " + this.name);
}
};
// ❌ PROBLEM: setTimeout extracts the function, loses this
setTimeout(user.greet, 1000);
// "Hello, undefined" — this = window, not user
// ✅ FIX 1: bind — locks this to user permanently
setTimeout(user.greet.bind(user), 1000);
// "Hello, Akshay"
// ✅ FIX 2: arrow function — captures this from surrounding scope
setTimeout(() => user.greet(), 1000);
// "Hello, Akshay"Partial Application with bind
function multiply(a, b) {
return a * b;
}
// Pre-fill the first argument. The bound function only needs the second:
const double = multiply.bind(null, 2); // a is always 2
const triple = multiply.bind(null, 3); // a is always 3
double(5); // 10 (2 * 5)
triple(5); // 15 (3 * 5)
double(100); // 200 (2 * 100)Critical Rule: bind Cannot Be Overridden
function sayName() { console.log(this.name); }
const bound = sayName.bind({ name: "Akshay" });
bound(); // "Akshay"
bound.call({ name: "Alok" }); // "Akshay" ← call can't override!
bound.bind({ name: "Pranav" })(); // "Akshay" ← second bind ignored!
// Exception: new CAN override bind (new has highest priority)
function Person(name) { this.name = name; }
const BoundPerson = Person.bind({ name: "Ignored" });
const p = new BoundPerson("Akshay");
p.name; // "Akshay" — new wins over bind!Polyfill of bind — Most Asked Polyfill in Interviews
Function.prototype.myBind = function(context, ...boundArgs) {
// 'this' here = the function being bound (e.g., greet in greet.myBind(obj))
const originalFn = this;
// Return a NEW function (bind doesn't call, it returns)
return function(...calledArgs) {
// When this new function is eventually called:
// - Use apply to set 'this' to context
// - Merge pre-filled args (boundArgs) with new args (calledArgs)
return originalFn.apply(context, [...boundArgs, ...calledArgs]);
};
};
// Test:
function greet(greeting, punct) {
console.log(greeting + ", " + this.name + punct);
}
const bound = greet.myBind({ name: "Akshay" }, "Hello");
bound("!"); // "Hello, Akshay!"Part 2 — Currying In Depth
What is Currying & Why Does It Exist?
Currying transforms a function that takes multiple arguments into a sequence of functions that each take a single argument:
f(a, b, c) becomes f(a)(b)(c)
Why it exists: Currying enables partial application — you can provide some arguments now and the rest later. This creates specialized, reusable functions from general ones. It’s the backbone of functional programming and is used heavily in libraries like Lodash, Ramda, and Redux.
Basic Currying — Manual Approach
// Normal: all args at once
function add(a, b, c) {
return a + b + c;
}
add(1, 2, 3); // 6
// Curried: one arg at a time, each returns a new function
function addCurried(a) {
return function(b) { // returns a function waiting for b
return function(c) { // returns a function waiting for c
return a + b + c; // finally computes the result
};
};
}
addCurried(1)(2)(3); // 6
// The magic — partial application:
const addOne = addCurried(1); // a is locked to 1
const addOneAndTwo = addOne(2); // b is locked to 2
addOneAndTwo(10); // 13 (1 + 2 + 10)
addOneAndTwo(20); // 23 (1 + 2 + 20) — reusable!Real-World Currying Examples
function discount(rate) {
return function(price) {
return price - (price * rate / 100);
};
}
const tenOff = discount(10); // 10% discount function
const twentyOff = discount(20); // 20% discount function
tenOff(500); // 450
tenOff(1000); // 900
twentyOff(500); // 400const buildUrl = (baseUrl) => (endpoint) => (id) =>
`${baseUrl}/${endpoint}/${id}`;
const api = buildUrl("https://api.example.com");
const usersApi = api("users");
const postsApi = api("posts");
usersApi(42); // "https://api.example.com/users/42"
postsApi(7); // "https://api.example.com/posts/7"const handleEvent = (eventType) => (element) => (callback) => {
element.addEventListener(eventType, callback);
};
const onClick = handleEvent("click");
const onClickButton = onClick(document.getElementById("btn"));
onClickButton(() => console.log("Clicked!"));Generic curry() Function — Works for Any Number of Args
function curry(fn) {
// fn.length = number of parameters the original function expects
return function curried(...args) {
if (args.length >= fn.length) {
// We have enough arguments — call the original function
return fn.apply(this, args);
} else {
// Not enough args yet — return a function that collects more
return function(...moreArgs) {
return curried.apply(this, [...args, ...moreArgs]);
};
}
};
}
function sum(a, b, c) { return a + b + c; }
const curriedSum = curry(sum);
curriedSum(1)(2)(3); // 6
curriedSum(1, 2)(3); // 6 — can pass multiple args at once too!
curriedSum(1)(2, 3); // 6
curriedSum(1, 2, 3); // 6 — works like the original tooThe Famous Infinite Currying: sum(1)(2)(3)...(n)()
function sum(a) {
return function(b) {
if (b !== undefined) {
return sum(a + b); // keep accumulating
}
return a; // no arg = return total
};
}
sum(1)(2)(); // 3
sum(1)(2)(3)(); // 6
sum(5)(10)(15)(20)(); // 50Part 3 — Promises In Depth
Why Do Promises Exist?
The Problem: Callback Hell
Before promises, async code used nested callbacks. When multiple async operations depended on each other, the code became deeply nested, hard to read, and hard to debug. This was called Callback Hell or the Pyramid of Doom:
getData(userId, function(user) {
getOrders(user.id, function(orders) {
getItems(orders[0].id, function(items) {
getPrice(items[0].id, function(price) {
console.log(price);
// 4 levels deep... and error handling makes it worse
});
});
});
});getData(userId)
.then(user => getOrders(user.id))
.then(orders => getItems(orders[0].id))
.then(items => getPrice(items[0].id))
.then(price => console.log(price))
.catch(err => console.log("Error:", err));Promise States & Lifecycle
The Three States
A promise is an object that represents the eventual result of an async operation. It has exactly three states:
- Pending — initial state. The operation hasn’t completed yet. Why: the async task is still running (e.g., HTTP request in flight).
- Fulfilled — the operation succeeded. The promise has a result
value.
.then()handlers run. Why: the data arrived successfully. - Rejected — the operation failed. The promise has a reason
(error).
.catch()handlers run. Why: something went wrong (network error, bad response, etc.).
Once a promise moves from Pending to Fulfilled or Rejected, it is settled — it cannot change state again. This is called immutability of settlement.
Creating a Promise
const myPromise = new Promise((resolve, reject) => {
// This function runs IMMEDIATELY (synchronously!)
// It receives two functions from JS engine:
// resolve(value) — call this when the operation succeeds
// reject(error) — call this when the operation fails
const success = true; // simulate some condition
if (success) {
resolve("Data loaded!"); // fulfills the promise
} else {
reject("Something failed"); // rejects the promise
}
});
// Consuming the promise:
myPromise
.then(value => console.log(value)) // runs if fulfilled
.catch(error => console.log(error)) // runs if rejected
.finally(() => console.log("Done")); // runs regardlessPromise Chaining — The Power of .then
// .then() always returns a new promise, so you can chain:
fetch("/api/user")
.then(response => response.json()) // returns a promise
.then(user => fetch(`/api/posts/${user.id}`)) // returns a promise
.then(response => response.json())
.then(posts => console.log(posts))
.catch(err => console.log("Error anywhere in the chain:", err));
// ^ One catch handles errors from ANY step above!The 4 Promise Static Methods — All of Them
Why do these exist?
Sometimes you have multiple promises running at the same time (parallel async operations) and need to combine their results. The four static methods handle different combinations:
Promise.all()— Wait for ALL to succeed. Resolves with array of all results. Rejects on FIRST failure. Use when: all results are needed, one failure means the whole operation fails.Promise.allSettled()— Wait for ALL to finish. Never rejects. Returns status + value/reason for each. Use when: you want results of all, even if some fail.Promise.race()— First to SETTLE wins. Returns result of the fastest promise (success or failure). Use when: you want the fastest response (e.g., timeout pattern).Promise.any()— First to SUCCEED wins. Ignores rejections until one succeeds. Rejects only if ALL fail (AggregateError). Use when: you have fallbacks (try multiple servers).
Promise.all() — In Depth
const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = Promise.resolve(3);
Promise.all([p1, p2, p3]).then(results => {
console.log(results); // [1, 2, 3] — all results, in ORDER (not speed)
});
// If ANY one rejects:
const p4 = Promise.reject("Fail!");
Promise.all([p1, p2, p4]).catch(err => {
console.log(err); // "Fail!" — first rejection kills everything
});Promise.allSettled() — In Depth
const p1 = Promise.resolve("Success");
const p2 = Promise.reject("Failed");
const p3 = Promise.resolve("Also success");
Promise.allSettled([p1, p2, p3]).then(results => {
console.log(results);
// [
// { status: "fulfilled", value: "Success" },
// { status: "rejected", reason: "Failed" },
// { status: "fulfilled", value: "Also success" }
// ]
});Promise.race() — In Depth
const fast = new Promise(res => setTimeout(() => res("Fast!"), 100));
const slow = new Promise(res => setTimeout(() => res("Slow!"), 500));
Promise.race([fast, slow]).then(result => {
console.log(result); // "Fast!" — first to settle wins
});
// Classic pattern: timeout for a fetch
const timeout = new Promise((_, reject) =>
setTimeout(() => reject("Timeout!"), 3000)
);
const fetchData = fetch("/api/data");
Promise.race([fetchData, timeout])
.then(res => console.log("Got data!"))
.catch(err => console.log(err)); // "Timeout!" if fetch takes > 3sPromise.any() — In Depth
const p1 = Promise.reject("Error 1");
const p2 = new Promise(res => setTimeout(() => res("Server 2"), 200));
const p3 = new Promise(res => setTimeout(() => res("Server 3"), 100));
Promise.any([p1, p2, p3]).then(result => {
console.log(result); // "Server 3" — first SUCCESS (p1 failed, ignored)
});
// Only rejects if ALL fail:
Promise.any([
Promise.reject("Fail 1"),
Promise.reject("Fail 2")
]).catch(err => {
console.log(err); // AggregateError: All promises were rejected
console.log(err.errors); // ["Fail 1", "Fail 2"]
});async/await — Syntactic Sugar over Promises
Why async/await exists
Promises with .then chains are better than callbacks, but they still
don’t read like normal synchronous code. async/await (ES2017) lets
you write async code that looks synchronous:
// Style 1: Callbacks
getUser(id, (user) => {
getPosts(user.id, (posts) => {
console.log(posts);
});
});
// Style 2: Promises
getUser(id)
.then(user => getPosts(user.id))
.then(posts => console.log(posts));
// Style 3: async/await — reads like synchronous code!
async function loadPosts(id) {
const user = await getUser(id); // pauses here until resolved
const posts = await getPosts(user.id); // pauses here until resolved
console.log(posts);
}Error Handling with async/await
async function fetchData() {
try {
const response = await fetch("/api/data");
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
console.log(data);
} catch (err) {
console.log("Error:", err.message);
// Catches: network errors, HTTP errors, JSON parse errors
} finally {
console.log("Cleanup"); // runs regardless
}
}Tricky Promise Output Questions
I/O 1. What will be the output? (Constructor is synchronous!)
console.log(1);
new Promise((resolve) => {
console.log(2);
resolve();
console.log(3); // ← does this run?
}).then(() => console.log(4));
console.log(5);Output: 1 → 2 → 3 → 5 → 4
Sync: 1, 2, 3 (yes! resolve() doesn’t stop execution!), 5. Microtask: 4 (.then callback).
I/O 2. What will be the output? (The ultimate event loop question)
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
Promise.resolve().then(() => setTimeout(() => console.log("D"), 0));
Promise.resolve().then(() => console.log("E"));
console.log("F");Output: A → F → C → E → B → D
Sync: A, F. Microtasks (all): C, then the second .then runs (schedules D as macrotask), E. Macrotasks: B (queued first), D (queued by microtask).
I/O 3. What will be the output? (async/await ordering)
async function foo() {
console.log("1");
const x = await "hello";
console.log("2");
}
console.log("3");
foo();
console.log("4");Output: 3 → 1 → 4 → 2
3 (sync). foo() starts → 1 (sync part). await pauses foo. 4 (sync
continues). Microtask: 2 (foo resumes after await). Even
await "hello" (a non-promise) creates a microtask pause.
I/O 4. What will Promise.all return here?
const p1 = new Promise(res => setTimeout(() => res("A"), 300));
const p2 = new Promise(res => setTimeout(() => res("B"), 100));
const p3 = new Promise(res => setTimeout(() => res("C"), 200));
Promise.all([p1, p2, p3]).then(console.log);Output: ["A", "B", "C"]
Even though p2 resolves first (100ms), results are in input order, not resolution order. Promise.all waits for the slowest (p1 at 300ms) then returns all results in the same order as the input array.
I/O 5. What will Promise.race return here?
const p1 = new Promise((_, rej) => setTimeout(() => rej("Error!"), 50));
const p2 = new Promise(res => setTimeout(() => res("Success!"), 100));
Promise.race([p1, p2])
.then(v => console.log("Resolved:", v))
.catch(e => console.log("Rejected:", e));Output: "Rejected: Error!"
p1 settles first (50ms) — but it’s a rejection. race returns the
first to settle regardless of outcome. So the catch runs. This is the
difference from Promise.any, which would wait for p2’s success.
Quick Revision — Everything You Need
call / apply / bind
call(ctx, a, b)= invoke NOW, comma args.apply(ctx, [a,b])= invoke NOW, array args.bind(ctx, a)= returns new function for LATER.- Why they exist: To explicitly set
thiswhen functions lose their object context (callbacks, extraction, borrowing). - bind is permanent — call/apply/second bind can’t override it. Only
newcan. - Arrow functions ignore call/apply/bind for
this— their this is always from the enclosing scope. - Polyfill trick: temporarily attach fn to context object, call as method, delete.
Currying
- Transforms
f(a,b,c)→f(a)(b)(c). Each call returns a new function waiting for the next arg. - Why it exists: Creates specialized reusable functions from general ones (partial application).
- Generic curry: check
args.length >= fn.length→ call or return collecting function. - Infinite currying:
sum(1)(2)...()— use recursive closure, empty call terminates. - Uses closures at every level to remember accumulated arguments.
Promises
- Why they exist: Solve callback hell, inversion of control, and error handling.
- 3 states: Pending → Fulfilled / Rejected. Settlement is permanent.
- Constructor runs synchronously.
.then/.catchare microtasks. resolve()does NOT stop execution inside the constructor!- Promise.all — all must succeed, first rejection fails all. Results in input order.
- Promise.allSettled — wait for all, never rejects. Returns {status, value/reason}.
- Promise.race — first to SETTLE (success or failure) wins.
- Promise.any — first to SUCCEED wins. Rejects only if ALL fail (AggregateError).
- async/await is sugar over promises.
awaitpauses function (not program), creates microtask. - Execution order: Sync → Microtasks (all promises) → Macrotasks (setTimeout).
Comments
Comments are disabled in this environment. Set
PUBLIC_GISCUS_REPO,PUBLIC_GISCUS_REPO_ID, andPUBLIC_GISCUS_CATEGORY_IDto enable.