JavaScript NotesRohit’s interview study guide
Chapter 02

Functions, Scope & this

Declarations vs expressions, closures, the four rules of this, call/apply/bind, currying and composition.

15 topics
Parts marked Advanced are extra depth. Skip them on a first read or a quick revision.

#Function declarations vs expressions vs arrows

JavaScript gives you three everyday ways to make a function, and they are not just style choices. A function declaration (function greet() {}) is a statement on its own line that creates a function and a binding with its name. A function expression (const greet = function () {}) creates a function as a value in the middle of another statement, usually an assignment. An arrow function (const greet = () => {}) is a shorter expression syntax that also changes how this, arguments and new behave.

The big practical difference between a declaration and an expression is hoisting. Before any line of a scope runs, the engine scans that scope and creates every binding declared in it. A function declaration is created complete — name and body — so you can call it above the line where it's written. A var is created too, but holding undefined; the function only gets assigned when execution reaches that line. let and const bindings are created but left uninitialised (the temporal dead zone, TDZ) until their line runs, and touching them before that throws.

Declaration — hoisted with its body

JavaScript
greet(); // "Hello!"

function greet() {
  console.log("Hello!");
}
What’s happening
  1. Before line 1 runs, the setup phase creates the binding greet and puts the whole function in it.
  2. greet() on line 1 finds a real function and logs "Hello!".
  3. When execution reaches the function greet line there is nothing left to do — it was handled during setup.

Expression — only the variable is hoisted

JavaScript
greet(); // TypeError: greet is not a function

var greet = function () {
  console.log("Hello!");
};
What’s happening
  1. During setup, var greet creates a binding holding undefined. The function itself doesn't exist yet — it's created only when the = function … line runs.
  2. greet() reads greet → undefined, then tries to call it. Calling a non-function is a TypeError.
  3. Any call placed after the assignment line would work, because by then greet holds the function.
AdvancedHoisting errors and arrow function differences

With var, greet exists but is undefined at the call, so you get a TypeError. With let/const you'd get a ReferenceError (TDZ) instead: Cannot access 'greet' before initialization. The different error type is a useful clue when debugging — TypeError means "the name exists but holds the wrong thing", ReferenceError means "you can't use this name here at all".

Which should you pick? Declarations suit named, top-level helpers: hoisting lets you put the main logic at the top of a file and the helpers underneath. Expressions (usually const plus an arrow) suit functions that are values — passed as callbacks, stored in objects, or chosen conditionally — and const stops the name being reassigned by accident.

Arrow functions are a shorter expression syntax with real behavioural differences. They were added in ES2015 mainly to make small callbacks lighter and to stop callbacks losing this:

Regular functionArrow function
Own thisYes — decided by how it's calledNo — uses this of the surrounding scope
arguments objectYesNo (use ...args)
Can be used with newYesNo — TypeError
Has prototypeYesNo
Good for object methodsYesUsually no
Good for callbacksNeeds bind if it uses thisYes

The this and arguments rows are the same rule: an arrow has no binding of its own, so the name is looked up in the enclosing function, exactly like a normal variable.

JavaScript
function outer() {
  const arrow = () => arguments[0];
  return arrow("arrow's own arg");
}
outer("outer's arg"); // "outer's arg"

const Arrow = () => {};
new Arrow();          // TypeError: Arrow is not a constructor
Arrow.prototype;      // undefined
What’s happening
  1. outer("outer's arg") runs. As a regular function it gets its own arguments object: ["outer's arg"].
  2. Inside, arrow("arrow's own arg") is called. The arrow has no arguments of its own, so arguments[0] is resolved up the scope chain and finds outer's arguments → "outer's arg". The value passed to the arrow is simply ignored.
  3. new Arrow() throws because arrows have no internal [[Construct]] behaviour — they can't create objects, so new refuses them up front.
  4. Arrow.prototype is undefined because a prototype object is only useful for constructors, and arrows can never be one.
  5. This shows the theme of arrows: fewer bindings of their own (this, arguments, prototype), which makes them ideal for callbacks and a poor fit for methods and constructors.
AdvancedArrow concise bodies and object literals
JavaScript
const square = (n) => n * n;          // implicit return
const makeUser = (name) => ({ name }); // wrap object literals in ( )
What’s happening
  1. square has a concise body: no braces after =>, so the expression n * n is returned automatically. square(4) → 16.
  2. In makeUser, a { straight after => would be read as the start of a block body, not an object. (name) => { name } is a block containing the useless statement name;, so it returns undefined.
  3. The ( ) forces the parser to read { name } as an expression — an object literal using shorthand — so makeUser("Rohit") → { name: "Rohit" }.

#Lexical scope

JavaScript uses lexical (static) scope: what a variable refers to is decided by where the code is written, not where it's called from. Each function can see its own variables, then its parent's, and so on up to the global scope — the scope chain.

Scope is fixed when the code is parsed. When a function is created, it records a link to the scope it was written inside. When the function later runs and meets a name, the engine looks in the function's own scope first, then follows that recorded link outwards, one level at a time, and stops at the first match. If it reaches the global scope without a match, you get a ReferenceError. Lookups only go outwards — an outer function can never see an inner function's variables.

A good mental model is a set of nested boxes drawn on the source code. Code inside a box can see out through its walls to every box around it, but nobody can see in. Where a function is called from has nothing to do with which boxes surround it.

JavaScript
const planet = "Earth";

function country() {
  const nation = "Canada";
  function city() {
    const town = "Toronto";
    return `${town}, ${nation}, ${planet}`;
  }
  return city();
}

country();         // "Toronto, Canada, Earth"
console.log(town); // ReferenceError: town is not defined
What’s happening
  1. Three nested scopes exist: global (planet), country (nation) and city (town). city is written inside country, which is written inside the global scope — that nesting is the scope chain.
  2. country() runs, creates nation = "Canada", then calls city().
  3. Inside city, town is found immediately in its own scope. nation isn't, so the lookup moves one level out to country → "Canada". planet needs two hops, to the global scope → "Earth". The result is "Toronto, Canada, Earth".
  4. At the top level, town is looked up in the global scope only. Lookups never go inward, so town is not found → ReferenceError. By this point city has also finished, but the error would happen even while it was running.
AdvancedScope fixed where code is written

The example below proves the "where it's written, not where it's called" part:

JavaScript
const name = "global";

function outer() {
  const name = "outer";
  function inner() {
    console.log(name); // "outer" — looks up the chain where inner was *written*
  }
  return inner;
}

function caller() {
  const name = "caller";
  outer()(); // still "outer", not "caller"
}
caller();
What’s happening
  1. There are three separate bindings called name: the global one ("global"), outer's ("outer") and caller's ("caller"). Each const lives in its own scope, so they don't clash — inner ones shadow outer ones.
  2. caller() runs and creates its name = "caller". It then calls outer().
  3. outer creates its own name = "outer", creates inner, and returns it. At creation, inner's outer link was fixed to outer's scope, because that's where it is written in the source.
  4. Back in caller, the second () calls the returned inner. It has no name of its own, so it follows its fixed link to outer's scope, finds "outer", and stops there.
  5. caller's name is never consulted, even though caller is on the call stack at that moment. The call stack only decides when code runs; the source layout decides what names mean.
AdvancedWhy lexical scope matters in practice

Why it matters in practice:

  • Closures depend on it. A function can keep using outer variables after the outer function returns because its scope link was fixed when it was written (see Closures).
  • Code is predictable. You can work out what any name refers to by reading the source, and so can linters, bundlers and minifiers — they safely rename local variables because nothing outside can depend on them.
  • Shadowing is the main trap. Declaring an inner variable with the same name as an outer one hides the outer one for that whole inner scope. It's legal, but it often hides bugs, which is why linters have a no-shadow rule.

#Closures

A closure is a function together with the variables it could see when it was created. Normally a function's local variables disappear when the function returns. But if an inner function that uses those variables is still around — because it was returned, stored, or passed as a callback — JavaScript keeps those variables alive for as long as the inner function exists.

A good mental model: every function carries an invisible backpack holding the variables from the scope it was written in. Wherever the function goes, the backpack goes with it, and it can read and change what's inside.

JavaScript
function makeCounter() {
  let count = 0; // private — nothing outside can touch it

  return {
    increment: () => ++count,
    decrement: () => --count,
    get value() {
      return count;
    },
  };
}

const counter = makeCounter();
counter.increment();
counter.increment();
console.log(counter.value); // 2
console.log(counter.count); // undefined — truly private
What’s happening
  1. makeCounter() runs and creates a local variable count with the value 0. On its own, count would vanish as soon as makeCounter returns.
  2. It returns an object with three functions: increment, decrement and the value getter. All three were written inside makeCounter, so all three have count in their backpack. That's why count survives after makeCounter has finished.
  3. counter.increment() runs ++count. It isn't working on a copy — it changes the one real count variable in the backpack: 0 → 1. The second call takes it 1 → 2.
  4. counter.value calls the getter, which reads the same count → 2.
  5. counter.count is undefined because count is a variable, not a property of the returned object. The only way to reach it is through the three functions — that's what makes it private. There is no syntax that lets outside code read or overwrite it.
AdvancedEach makeCounter() call gets new state

Every call to makeCounter() runs the function body again, so it creates a brand-new count and a brand-new backpack:

JavaScript
const a = makeCounter();
const b = makeCounter();

a.increment();
a.increment();
b.increment();

console.log(a.value); // 2
console.log(b.value); // 1 — b has its own count
What’s happening
  1. makeCounter() is called twice, so there are now two separate count variables, each starting at 0.
  2. a's functions closed over the first count; b's functions closed over the second. They never see each other's.
  3. a.increment() twice → first count is 2. b.increment() once → second count is 1.
  4. This is exactly how objects with private state worked before classes had #private fields — and it's still how factory functions and React hooks keep per-instance state.
AdvancedWhere closures show up in real code

Where closures show up in real code:

  • Private state — the module pattern and factory functions (above).
  • Function factories — add(5) returns a function that remembers 5 (see Currying).
  • Callbacks and event handlers — a click handler created inside a loop or a component remembers the values from when it was created.
  • Debounce, throttle, memoize — each keeps its timer or cache in a closure, so it survives between calls (see Debouncing).
  • React hooks — useState and useEffect callbacks are closures over that render's props and state, which is also why you can get "stale closure" bugs.
The classic loop question
JavaScript
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
Show answer

Prints 3 3 3.

What’s happening
  1. var is function-scoped, so the whole loop has exactly one i variable.
  2. The loop runs three times very quickly and schedules three callbacks. Each callback closes over that same i — the backpack holds the variable itself, not its value at that moment.
  3. After the third iteration, i++ makes i equal 3, the condition 3 < 3 is false, and the loop ends.
  4. Only now does the event loop run the timers (they're macrotasks, so they wait for the current code to finish). All three callbacks read the one shared i, which is 3.

Change var to let and it prints 0 1 2. let is block-scoped, and a for (let …) loop creates a fresh i for every iteration (copying the current value across). Each callback closes over its own i — 0, 1 and 2.

Before let existed, the fix was an IIFE that passes the current value in as a parameter, which creates a new variable j per iteration:

JavaScript
for (var i = 0; i < 3; i++) {
  ((j) => setTimeout(() => console.log(j), 0))(i);
}
AdvancedPitfall: closures keeping data alive

#IIFE and the module pattern

An IIFE (Immediately Invoked Function Expression) runs as soon as it's defined. It creates a private scope so variables don't leak into the global namespace.

It exists because of how scripts used to work. Before ES2015 there was no let, const or modules, and every var or function declaration at the top level of a <script> became a property of window, shared with every other script on the page. Two libraries that both declared var config silently overwrote each other. The only thing that created a new scope was a function, so the trick was: write a function, call it immediately, and never give it a name — its variables live and die inside it, and nothing leaks out.

The parentheses around the function are not decoration. A statement that starts with the keyword function is parsed as a declaration, so function () {}() is a SyntaxError: a declaration needs a name, and even function foo() {}() fails because a declaration can't be called in place. Wrapping it in ( ) makes the parser read it as an expression that produces a function value, which the trailing () then calls.

JavaScript
(function () {
  let a = 10;
  console.log(a); // 10
})();

console.log(a); // ReferenceError: a is not defined

// Arrow and async versions
(() => { /* ... */ })();
(async () => {
  const data = await fetch("/api").then((r) => r.json());
})();
What’s happening
  1. (function () { … }) evaluates to a function object; the trailing () calls it straight away. The function has no name and nothing stores it, so it can't be called a second time.
  2. Inside, let a = 10 is created in the function's own scope and console.log(a) prints 10.
  3. The function returns. Nothing references its scope any more, so a becomes unreachable and can be garbage-collected.
  4. The outer console.log(a) searches the outer scope chain, which never included the IIFE's insides → ReferenceError: a is not defined. That's the whole point: no leak.
  5. The arrow version works the same way. The async version exists because await is only allowed inside an async function (or at the top level of an ES module since ES2022). Wrapping code in an async IIFE lets a classic script or CommonJS file use await. The IIFE returns a promise immediately and the rest of the file carries on without waiting, so catch errors inside it.
AdvancedPitfall: IIFE after a missing semicolon
AdvancedThe module pattern: IIFE plus closure

The module pattern combines an IIFE with a closure to expose a public API and keep everything else private — this is how libraries were written before ES modules.

JavaScript
const BankModule = (function () {
  let balance = 0; // private

  function log(msg) { // private helper
    console.log(`[bank] ${msg}`);
  }

  return {
    deposit(amount) {
      balance += amount;
      log(`deposited ${amount}`);
    },
    getBalance() {
      return balance;
    },
  };
})();

BankModule.deposit(100);
BankModule.getBalance(); // 100
BankModule.balance;      // undefined
What’s happening
  1. The IIFE runs once, right away, while BankModule is being initialised. Inside it, balance is set to 0 and the helper log is created.
  2. It returns an object with deposit and getBalance. Both are written inside the IIFE, so both close over balance and log. That returned object is what lands in BankModule; the IIFE itself is gone.
  3. BankModule.deposit(100) updates the one real balance: 0 → 100. It then calls the private log, which prints [bank] deposited 100.
  4. BankModule.getBalance() reads the same balance → 100.
  5. BankModule.balance is undefined because balance is a variable in the IIFE's scope, not a property of the returned object. Outside code can only go through the two public methods, so nobody can write balance = -500.
  6. Compare with makeCounter in Closures: a factory can be called many times to make many instances, but an IIFE runs exactly once, so the module pattern always produces a singleton.
AdvancedWhere IIFEs still appear

Where you'll still see it: old jQuery plugins, UMD library builds (one file that works as a script tag, in CommonJS and in AMD), and bundler output (webpack, for example, wraps each module in a function for the same isolation). The trade-offs are the flip side of the privacy: private helpers can't be unit-tested or patched from outside, and there's only ever one instance. A common variant is the revealing module pattern, where every function is written as a private local and the returned object just lists the ones to expose: return { deposit, getBalance }.

AdvancedES modules replace the IIFE pattern

#How this works

this in a regular function is decided at call time, by how the function is called. Check the rules in this order:

  1. new binding — new Foo() → this is the brand-new object.
  2. Explicit binding — fn.call(obj), fn.apply(obj), fn.bind(obj) → this is obj.
  3. Implicit binding — obj.fn() → this is the object before the dot.
  4. Default binding — plain fn() → undefined in strict mode, globalThis in sloppy mode.

Arrow functions ignore all four rules and use this from the scope they were written in.

Think of this as a hidden parameter that every regular function receives. Ordinary parameters are filled from the argument list; this is filled from the call site — the expression that does the calling. That is the opposite of normal variables, which are lexical (see Lexical scope). It works this way so one function can be shared by many objects: a single method on a prototype serves every instance, and this tells it which instance it's working for on this particular call.

So never ask "where was this function written?" to find this. Ask "how is it being called, right now?" and walk down the four rules. Note that ES modules and class bodies are always in strict mode, so in modern code default binding gives undefined.

JavaScript
function whoAmI() {
  return this?.label;
}

const obj = { label: "obj", whoAmI };
const other = { label: "other" };

whoAmI();                   // undefined — default binding (strict mode)
obj.whoAmI();               // "obj"     — implicit
obj.whoAmI.call(other);     // "other"   — explicit beats implicit

obj.bound = whoAmI.bind(other);
obj.bound();                // "other"   — a bound function ignores the dot

function Point() {
  this.label = "point";
}
const BoundPoint = Point.bind(other);
const p = new BoundPoint();
p.label;                    // "point"   — new beats bind
other.label;                // "other"   — bind's object untouched
What’s happening
  1. whoAmI() — no dot, no call/apply/bind, no new, so default binding applies. In a module (strict mode) this is undefined and this?.label → undefined; without the ?. it would throw a TypeError. In a sloppy script this would be globalThis, whose label is also undefined.
  2. obj.whoAmI() — the same function, now called with obj. in front → implicit binding, this is obj → "obj". The shorthand { whoAmI } stored a reference to the very same function; nothing was copied.
  3. obj.whoAmI.call(other) — there is still a dot, but .call says explicitly that this must be other → "other". Explicit beats implicit.
  4. whoAmI.bind(other) returns a new function with this locked to other. Calling it as obj.bound() would normally make this obj, but a bound function ignores whatever it's called on → "other".
  5. new BoundPoint() — new creates a fresh empty object (whose prototype is Point.prototype) and runs Point with this set to it, overriding the bound other. So p.label is "point" and other.label is still "other". That's why new sits at the top of the list.
AdvancedImplicit, lexical, default and explicit this
JavaScript
const user = {
  name: "Rohit",
  regular() {
    return this.name;
  },
  arrow: () => this?.name, // `this` of the module/global scope, not user
};

user.regular();        // "Rohit"   (implicit)
user.arrow();          // undefined (lexical)

const fn = user.regular;
fn();                  // undefined / TypeError — lost its object (default binding)
fn.call({ name: "X" }); // "X" (explicit)
What’s happening
  1. user.regular() — user. sits in front of the call → implicit binding, this is user → "Rohit".
  2. user.arrow() — the dot is ignored because arrows have no this of their own. An object literal's braces are not a function and don't create a this, so the arrow uses this from the top level of the file. In an ES module that's undefined, so this?.name → undefined (in Node CommonJS it's module.exports, {}, which gives undefined too). In a classic browser <script> it would be window, and window.name is a real property — usually "" — so you'd get "", not undefined.
  3. const fn = user.regular copies a reference to the function, nothing more. Functions don't remember which object they were read from.
  4. fn() is a plain call → default binding. In strict mode (modules, classes) this is undefined, so this.name throws TypeError: Cannot read properties of undefined (reading 'name'). In sloppy mode this is globalThis, so you get globalThis.name — undefined in Node, "" in a browser. That's what "undefined / TypeError" in the comment means.
  5. fn.call({ name: "X" }) — explicit binding supplies the object for this one call → "X".
AdvancedPitfall: methods passed as callbacks

#Lexical this with arrow functions

Before ES2015, every function had its own this, so a callback inside a method got a different this from the method around it — whatever the caller of the callback chose. The workarounds were var self = this; before the callback, or .bind(this) on it. Arrow functions fix this by not having a this binding at all: inside an arrow, this is looked up through the scope chain like any other variable, and it finds the this of the nearest enclosing regular function (or the module/script top level).

The mental model: an arrow borrows this from the function it was created in, as it was for that particular call. It doesn't lock onto "the object" — it inherits whatever this the surrounding function had when the arrow was made.

Arrow functions are perfect inside methods, where you want the callback to share the method's this:

JavaScript
const timer = {
  seconds: 0,
  start() {
    // Old way: const self = this; function () { self.seconds++ }
    setInterval(() => {
      this.seconds++; // `this` is timer, inherited from start()
    }, 1000);
  },
};
timer.start();
What’s happening
  1. timer.start() is a method call, so inside start implicit binding makes this equal timer.
  2. During that call, the arrow is created and handed to setInterval. It has no this of its own; when its body mentions this, the lookup goes to the enclosing function — start — and finds timer.
  3. Every 1000 ms the runtime calls the arrow, passing its own this (window in browsers, the Timeout object in Node). The arrow ignores it, so this.seconds++ updates timer.seconds: 0 → 1 → 2 → ….
  4. If you wrote function () { this.seconds++; } instead, this would be that window/Timeout object. this.seconds is undefined there, undefined + 1 is NaN, and timer.seconds stays 0 forever — no error, just wrong.
  5. If you changed the call to const s = timer.start; s();, start's own this would be undefined (strict mode), so the arrow inherits undefined too and throws on the first tick. Arrows pass on this; they don't fix a lost one.
AdvancedWhen arrows are the wrong choice

But they're the wrong choice as a method, or when a library sets this for you:

JavaScript
const counter = {
  count: 0,
  inc: () => this.count++, // ❌ `this` is not counter
};

button.addEventListener("click", function () {
  this.classList.toggle("on"); // ✅ `this` is the button
});
button.addEventListener("click", () => {
  this.classList.toggle("on"); // ❌ `this` is the outer scope
});
What’s happening
  1. inc is an arrow inside an object literal, and an object literal doesn't provide a this. The arrow uses the file's top-level this. In a module that's undefined, so counter.inc() throws TypeError: Cannot read properties of undefined (reading 'count'). In a classic browser script it's window, so window.count++ silently turns window.count into NaN while counter.count stays 0. The fix is method shorthand: inc() { this.count++; }.
  2. With a regular function listener, the DOM calls it with this set to event.currentTarget — the element the listener is attached to. So this is the button and the class toggles.
  3. With an arrow listener, the DOM's this is ignored and the arrow uses the outer this (undefined in a module, window in a script). Neither has a classList, so it throws a TypeError.
  4. The robust option is to take the event parameter and use event.currentTarget.classList.toggle("on"). It works with both function styles and makes the intent obvious.
AdvancedRule of thumb: method or callback

Rule of thumb: when the function is the method, use a regular function (method shorthand). When the function is a callback inside a method and needs the method's this, use an arrow. In classes, arrow-function class fields are the exception that combines both (see Real use cases for bind).

#call, apply and bind

All three let you choose what this is.

They are methods on Function.prototype, so every regular function has them. They exist because this is normally chosen by the call site, and sometimes the call site is not what you want: you want to run a function against an object it isn't attached to, or hand a method to code that will call it later without its object. call and apply mean "run this function now, as if it were a method of that object". bind means "give me a new version of this function that is permanently glued to that object" (and, optionally, to some leading arguments).

MethodCalls the function?ArgumentsReturns
fn.call(thisArg, a, b)ImmediatelyOne by oneFunction's result
fn.apply(thisArg, [a, b])ImmediatelyAs an arrayFunction's result
fn.bind(thisArg, a, b)NoOne by one (pre-filled)A new bound function
JavaScript
function introduce(greeting, punctuation) {
  return `${greeting}, I'm ${this.name}${punctuation}`;
}
const rohit = { name: "Rohit" };

introduce.call(rohit, "Hi", "!");      // "Hi, I'm Rohit!"
introduce.apply(rohit, ["Hello", "."]); // "Hello, I'm Rohit."

const sayHi = introduce.bind(rohit, "Hey");
sayHi("?");                             // "Hey, I'm Rohit?"
What’s happening
  1. introduce is a plain function that reads this.name, and it doesn't belong to any object. Called as introduce("Hi", "!") in strict mode, this would be undefined and it would throw.
  2. introduce.call(rohit, "Hi", "!") — the first argument becomes this, the rest fill the parameters in order: this = rohit, greeting = "Hi", punctuation = "!". It runs immediately and returns "Hi, I'm Rohit!".
  3. introduce.apply(rohit, ["Hello", "."]) — identical, except the arguments arrive as one array that apply unpacks: greeting = "Hello", punctuation = "." → "Hello, I'm Rohit.".
  4. introduce.bind(rohit, "Hey") does not call introduce. It returns a new function, sayHi, that remembers this = rohit and a first argument of "Hey". introduce itself is unchanged.
  5. sayHi("?") calls introduce with this = rohit and the pre-filled arguments followed by the new ones: ["Hey", "?"] → "Hey, I'm Rohit?".
  6. If you pass null or undefined as thisArg, strict-mode functions get exactly that as this; sloppy-mode functions get globalThis instead (and primitives like 5 get wrapped in objects). That's why Math.max.apply(null, nums) is fine — Math.max never reads this.
AdvancedWhen to use call, apply or bind

Mnemonic: Call = Commas, Apply = Array.

When to use which: call when you know the arguments and want to run the function once with a chosen this (function borrowing, calling a parent constructor in pre-class code). apply when the arguments are already in an array — though today fn.call(obj, ...args) does the same. bind when you're handing the function to someone else to call later and can't control how they'll call it.

AdvancedBound this can't be rebound

#Function borrowing

Use a method from one object on another object, without copying it.

This works because in JavaScript a "method" is just a function stored in a property. The function isn't owned by the object, and this is decided per call. So any function that only touches this.something will work on any object that has those properties — "if it has a firstName and a lastName, it can be named". Borrowing means calling that function with call or apply and a different this, without copying it onto the other object, inheriting from anything, or mutating anything.

JavaScript
const person1 = {
  firstName: "John",
  lastName: "Doe",
  fullName() {
    return `${this.firstName} ${this.lastName}`;
  },
};
const person2 = { firstName: "Jane", lastName: "Smith" };

person1.fullName.call(person2); // "Jane Smith"
What’s happening
  1. person1.fullName only reads the function from person1 — no call has happened yet.
  2. .call(person2) then invokes it with this = person2. Inside, this.firstName → "Jane" and this.lastName → "Smith", giving "Jane Smith".
  3. person2 never gains a fullName property. Nothing was copied or changed; person2.fullName() would still throw TypeError: person2.fullName is not a function.
  4. It works only because fullName depends on nothing but this.firstName and this.lastName. If it used a variable private to person1, borrowing wouldn't help.
AdvancedReal-life function borrowing

Real-life uses:

JavaScript
// 1. A generic function shared by many objects
function calculateArea() {
  return this.length * this.width;
}
calculateArea.call({ length: 10, width: 5 }); // 50
calculateArea.call({ length: 7, width: 3 });  // 21

// 2. Array methods on array-like objects (arguments, NodeList)
function oldSchool() {
  return Array.prototype.slice.call(arguments); // today: Array.from(arguments) or [...arguments]
}

// 3. A reliable type check
Object.prototype.toString.call([]);   // "[object Array]"
Object.prototype.toString.call(null); // "[object Null]"
What’s happening
  1. calculateArea is written to be borrowed — it isn't attached to any object. .call({ length: 10, width: 5 }) makes this that object → 10 * 5 = 50. The second call gets a different this → 7 * 3 = 21.
  2. arguments has numbered keys and a length, but it isn't an array, so it has no slice. Array.prototype.slice only needs its this to have a length and numbered keys, so slice.call(arguments) with no start or end copies everything into a real array: oldSchool(1, 2, 3) → [1, 2, 3]. The same trick converted DOM NodeLists before Array.from existed.
  3. Every object inherits toString from Object.prototype, but arrays, dates and others override it ([].toString() is ""). Borrowing the original gives the value's internal tag: "[object Array]", "[object Date]", "[object Null]". The spec special-cases null and undefined, so it works even for them.
  4. It beats typeof, which reports "object" for both null and []. (Objects can customise the tag with Symbol.toStringTag, so it's not tamper-proof, but it's reliable for built-ins.)

#The problem apply solved (and spread replaced)

Before ES2015 there was no way to pass an array as separate arguments — apply was the trick.

Functions like Math.max and Array.prototype.push accept any number of arguments but not an array: Math.max([5, 1, 9, 3]) converts the array to the string "5,1,9,3" and returns NaN. Since apply takes its arguments as an array and unpacks them into the call, it was borrowed purely for that unpacking, with this set to whatever the function needed. ES2015's spread syntax does the unpacking directly at the call site, works with any iterable (not just arrays), and also works with new.

JavaScript
const nums = [5, 1, 9, 3];
Math.max.apply(null, nums); // 9
Math.max(...nums);          // 9 — spread does the same today

const arr1 = [1, 2];
const arr2 = [3, 4];
Array.prototype.push.apply(arr1, arr2); // same as arr1.push(...arr2)
What’s happening
  1. Math.max.apply(null, nums) unpacks the array and becomes Math.max(5, 1, 9, 3) → 9. null is passed as this only because apply needs something in that slot; Math.max never reads this.
  2. Math.max(...nums) performs the same unpacking with syntax → the same call → 9, with no this to think about.
  3. Array.prototype.push.apply(arr1, arr2) calls push with this = arr1 and arguments 3, 4 — exactly arr1.push(3, 4). arr1 becomes [1, 2, 3, 4], arr2 is unchanged, and the return value is 4, the new length.
  4. Without unpacking, arr1.push(arr2) would push the whole array as one item: [1, 2, [3, 4]].
  5. Note that this mutates arr1. If you want a new array instead, use [...arr1, ...arr2] or arr1.concat(arr2).
AdvancedSpreading huge arrays into a call

#Real use cases for bind

bind is for the moment you hand a function to code you don't control — a timer, the DOM, a framework, a library — that will call it later in its own way. You can't change their call site, so you fix this (and optionally some leading arguments) in advance. The second use, pre-filling arguments, is called partial application.

JavaScript
// 1. Keeping `this` for callbacks
class Clock {
  constructor() {
    this.ticks = 0;
    this.tick = this.tick.bind(this); // bind once in the constructor
  }
  tick() {
    this.ticks++;
  }
  start() {
    setInterval(this.tick, 1000); // safe to pass around
  }
}

// 2. Partial application — pre-fill arguments
function log(level, message) {
  console.log(`[${level}] ${message}`);
}
const warn = log.bind(null, "WARN");
warn("Disk almost full"); // [WARN] Disk almost full

// 3. Event handlers that need the component instance
button.addEventListener("click", this.handleClick.bind(this));
What’s happening
  1. new Clock() runs the constructor with this = the new instance. On the right of this.tick = this.tick.bind(this), this.tick isn't on the instance yet, so it's read from Clock.prototype. bind makes a copy locked to this instance, and the assignment stores it as an own property that shadows the prototype method.
  2. start() passes this.tick, which is now the bound own property. setInterval calls it as a plain function every second, but because it's bound, this is still the instance: ticks goes 0 → 1 → 2 → ….
  3. Without the bind line, the timer would call the prototype tick with its own this (window or Node's Timeout). The increments would land on that object as NaN and clock.ticks would stay 0.
  4. log.bind(null, "WARN") — log doesn't use this, so null fills the slot. "WARN" is pre-filled as level, so warn("Disk almost full") becomes log("WARN", "Disk almost full") → [WARN] Disk almost full.
  5. this.handleClick.bind(this) (inside a class method) makes the listener's this the component instance instead of the button. But every bind call returns a new function, so removeEventListener("click", this.handleClick.bind(this)) removes nothing. If you need to remove it later, store the bound function once, as Clock does.
AdvancedArrow class fields instead of bind

#Higher-order functions

A higher-order function takes a function as an argument, returns a function, or both. map, filter, reduce, forEach, setTimeout and addEventListener are all higher-order functions.

This is possible because functions in JavaScript are first-class values: you can store them in variables, put them in arrays and objects, pass them to other functions and return them, just like numbers or strings. A higher-order function is like a template with a hole in it. The higher-order function handles the how and when (loop over items, wait 100 ms, run on click), and the function you pass in fills the hole with the what. That split lets you write the loop or the timing logic once and reuse it for any behaviour.

JavaScript
function greet(name) {
  return function (message) {
    console.log(`${message}, ${name}`);
  };
}

const greetAlice = greet("Alice");
greetAlice("Hello");   // Hello, Alice
greetAlice("Goodbye"); // Goodbye, Alice

// Takes a function
function repeat(n, action) {
  for (let i = 0; i < n; i++) action(i);
}
repeat(3, console.log); // 0 1 2
What’s happening
  1. greet("Alice") runs with name = "Alice" and returns a new function. Nothing is logged yet — greet only builds the greeter. The returned function closes over name.
  2. greetAlice("Hello") runs that inner function with message = "Hello"; name comes from the closure → logs Hello, Alice. greetAlice("Goodbye") → Goodbye, Alice. greet itself ran only once.
  3. repeat(3, console.log) passes the function console.log itself — no parentheses, so it isn't called at this point.
  4. Inside, the loop runs with i = 0, 1, 2 and calls action(i) each time, which is console.log(0), console.log(1), console.log(2) → three lines.
  5. repeat knows nothing about logging. The looping is written once and the caller decides what happens on each pass — exactly how arr.forEach(fn) works.
AdvancedWrapping a function with withLogging

The most useful higher-order functions do both: take a function and return an improved version of it. This is how debounce, throttle, memoize and once are built:

JavaScript
function withLogging(fn) {
  return function (...args) {
    const result = fn.apply(this, args);
    console.log(`${fn.name}(${args.join(", ")}) → ${result}`);
    return result;
  };
}

const add = (a, b) => a + b;
const loggedAdd = withLogging(add);
loggedAdd(2, 3); // logs "add(2, 3) → 5", returns 5
What’s happening
  1. withLogging(add) doesn't call add. It returns a new wrapper function that closes over fn = add.
  2. loggedAdd(2, 3) runs the wrapper; the rest parameter collects args = [2, 3].
  3. fn.apply(this, args) calls the original add(2, 3) → 5. Forwarding this means the wrapper would also work around an object method, not just a plain function.
  4. It logs add(2, 3) → 5 (fn.name is "add", inferred from the const it was assigned to) and returns 5, so callers can't tell the difference except for the log.
  5. add itself is untouched. You've added behaviour around it without editing it — the same idea as decorators and Express middleware.
AdvancedWhere higher-order functions show up

Where you'll use them: array methods everywhere, event and timer APIs, wrappers like the one above, Express middleware (app.use(fn)) and React's higher-order components (a function that takes a component and returns a new one). The cost is indirection: several layers of returned functions make stack traces longer and code harder to follow, so reach for them when they remove real repetition.

#Currying

Currying turns a function of several arguments into a chain of functions that each take one argument.

Instead of add(2, 3) you write add(2)(3): each call takes one argument and returns a new function waiting for the next, until the last argument arrives and the real work is done. The earlier arguments are remembered by closures. (It's named after the logician Haskell Curry.)

The mental model is a form filled in one field at a time: every partly filled form is itself a reusable template. add(5) is "an adder with the first field set to 5" — you can keep it and use it many times. That's the real point: currying lets you create specialised functions from general ones, and it produces one-argument functions, which is exactly the shape compose and pipe need.

JavaScript
function add(a) {
  return function (b) {
    return a + b;
  };
}

const addFive = add(5);
addFive(3);  // 8
add(2)(3);   // 5

const addArrow = (a) => (b) => a + b; // same thing
What’s happening
  1. add(5) runs with a = 5 and returns the inner function. add has finished, but a lives on in the inner function's closure.
  2. addFive(3) runs the inner function with b = 3, reads a = 5 from the closure → 8. You can call addFive as often as you like; a stays 5.
  3. add(2)(3) does both steps in one expression: add(2) returns a function and the second (3) calls it straight away → 5. That call creates a fresh closure with a = 2, completely separate from addFive's.
  4. addArrow is the same thing with arrows. Read (a) => (b) => a + b as "take a, return a function that takes b and returns a + b".
AdvancedWriting a generic curry helper

A generic curry helper is a popular interview question:

curry.js
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }
    return (...more) => curried.apply(this, [...args, ...more]);
  };
}

const volume = curry((l, w, h) => l * w * h);
volume(2)(3)(4);   // 24
volume(2, 3)(4);   // 24
volume(2)(3, 4);   // 24
What’s happening
  1. curry(fn) returns curried, which remembers fn in its closure. fn.length is the number of declared parameters — 3 for (l, w, h) — and it's how the helper knows when it has collected enough.
  2. volume(2) → args = [2]. 1 >= 3 is false, so it returns an arrow that remembers [2].
  3. (3) → the arrow calls curried again with [...[2], ...[3]] = [2, 3]. 2 >= 3 is false → another arrow, this one remembering [2, 3].
  4. (4) → curried gets [2, 3, 4]. 3 >= 3 is true, so it calls fn.apply(this, [2, 3, 4]) → 2 * 3 * 4 = 24.
  5. volume(2, 3)(4) collects [2, 3] then [2, 3, 4]; volume(2)(3, 4) collects [2] then [2, 3, 4]. Only the total count matters, not how the arguments are grouped, and volume(2, 3, 4) runs at once.
  6. Partials are independent and reusable: with const v2 = volume(2), both v2(3)(4) → 24 and v2(5)(6) → 60 work. That's because [...args, ...more] builds a new array each time instead of pushing into the shared args.
  7. The apply(this, …) calls, together with the arrow (which inherits curried's this), pass along whatever this the curried function was called with, so it still works as a method.
AdvancedPitfall: defaults and rest in fn.length
AdvancedWhy curry: specialised functions

Why bother? It makes specialised functions easy to create (const withTax = multiply(1.18)) and fits naturally with compose/pipe.

JavaScript
const multiply = (a) => (b) => a * b;
const withTax = multiply(1.18);

[100, 250].map(withTax); // [118, 295]
What’s happening
  1. multiply(1.18) fixes the rate once and returns a one-argument function withTax, with a = 1.18 in its closure.
  2. map calls withTax(100) → 118, then withTax(250) → 295. (map also passes the index and the array, but withTax only declares b, so the extras are ignored.)
  3. Without currying you'd write map((price) => multiply(1.18, price)) at every call site. The curried version names the idea once and reuses it.

#Function composition: compose and pipe

compose(f, g, h)(x) means f(g(h(x))) — functions run right to left. pipe is the same idea left to right, which many people find easier to read.

Composition builds a bigger function out of smaller ones: the output of one becomes the input of the next. It exists so you can write small, single-purpose, easily tested functions and snap them together, instead of nesting calls like subtract10(double(add1(x))), which read inside-out, or creating a temporary variable for every step.

Picture an assembly line. pipe lists the stations in the order the item visits them. compose lists them the way maths writes f ∘ g — outermost first — which is why it runs right to left. Either way, every station must take one value and return one value for the next station, which is why curried functions slot in so well.

JavaScript
const add1 = (n) => n + 1;
const double = (n) => n * 2;
const subtract10 = (n) => n - 10;

const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x);
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);

compose(subtract10, double, add1)(3); // (3 + 1) * 2 - 10 = -2
pipe(add1, double, subtract10)(3);    // same: -2
What’s happening
  1. compose(subtract10, double, add1) runs nothing yet. It returns a function that closes over fns = [subtract10, double, add1].
  2. Calling it with 3 starts reduceRight with acc = 3 at the last function: add1(3) → 4.
  3. Moving left: double(4) → 8. Then subtract10(8) → -2. That's exactly subtract10(double(add1(3))).
  4. pipe(add1, double, subtract10) stores the same functions in reading order, and reduce walks them left to right: 3 → 4 → 8 → -2. Same result, but the code lists the steps in the order they happen.
  5. Order matters: pipe(add1, subtract10, double)(3) goes 3 → 4 → -6 → -12. And with no functions at all, compose()(5) returns 5 — the reducer has nothing to apply, so it hands back the starting value.
AdvancedData pipelines with currying and pipe

Currying and pipe together give you readable data pipelines:

JavaScript
const map = (fn) => (arr) => arr.map(fn);
const filter = (fn) => (arr) => arr.filter(fn);
const sum = (arr) => arr.reduce((a, b) => a + b, 0);

const totalOfEvenSquares = pipe(
  filter((n) => n % 2 === 0),
  map((n) => n * n),
  sum,
);
totalOfEvenSquares([1, 2, 3, 4]); // 20
What’s happening
  1. map and filter are curried wrappers around the array methods: give them the callback first and you get back a one-argument function that waits for the array. That's what makes them fit in pipe.
  2. filter((n) => n % 2 === 0) returns a function that keeps even numbers; map((n) => n * n) returns one that squares; sum is already one-argument. pipe returns a function closing over all three.
  3. Calling it with [1, 2, 3, 4]: the filter step → [2, 4].
  4. The map step → [4, 16]. The sum step → 0 + 4 + 16 = 20.
  5. Each step is tiny and testable alone, and totalOfEvenSquares reads like a description of what it does.
Advancedcompose and pipe in libraries

You'll meet this in Redux (its compose combines store enhancers), Lodash (flow is pipe, flowRight is compose) and Ramda. These versions assume every step is synchronous; with async steps each fn(acc) returns a promise, so you'd need acc.then(fn) instead. When a pipeline is only two or three steps used once, plain nested calls or intermediate consts are often clearer.

Advancedcompose as a reduceRight one-liner

#Rest parameters and spread

They use the same ... syntax but do opposite jobs: rest collects many values into an array, spread expands an iterable into individual values.

Which one you're looking at depends on where the dots are. Where values are being received — a parameter list, or the left side of a destructuring pattern — ... is rest: "gather whatever is left into one array (or object)". Where values are being supplied — call arguments, an array literal, an object literal — ... is spread: "tip this collection out into separate items". Rest replaced the old arguments object; spread replaced most uses of apply, concat, slice() and Object.assign. Array spread and rest arrived in ES2015, object spread and rest in ES2018.

JavaScript
// Rest: gather arguments
function sum(...nums) {
  return nums.reduce((acc, n) => acc + n, 0);
}
sum(1, 2, 3, 4); // 10

function tag(first, ...others) {} // rest must be the last parameter

// Spread: expand
const defaults = { theme: "light", lang: "en" };
const options = { theme: "dark" };
const a = [1, 2];
const b = [3, 4];
const combined = [...a, ...b];          // [1, 2, 3, 4]
const copy = [...a];                    // shallow copy
const chars = [..."hey"];               // ["h", "e", "y"]
const merged = { ...defaults, ...options }; // later keys win
Math.max(...combined);                  // 4
What’s happening
  1. sum(1, 2, 3, 4) — the rest parameter gathers every argument into a real array, nums = [1, 2, 3, 4]. reduce then runs 0 → 1 → 3 → 6 → 10.
  2. In tag(first, ...others), named parameters are filled first and the rest gets the leftovers: tag("a", "b", "c") gives first = "a", others = ["b", "c"]. tag("a") gives others = [] — always an array, never undefined. Anything after a rest parameter is a SyntaxError, because it could never receive a value.
  3. [...a, ...b] tips each array's items into a new literal → [1, 2, 3, 4]. a and b are untouched. [...a] is a new array with the same items — a shallow copy, so if the items were objects, both arrays would point to the same objects.
  4. [..."hey"] works because strings are iterable → ["h", "e", "y"]. The string iterator goes by code point, so [..."👍a"] has 2 items, while "👍a".split("") has 3 (the emoji is two UTF-16 units).
  5. { ...defaults, ...options } copies own enumerable properties left to right. theme appears twice, and the later one wins → { theme: "dark", lang: "en" }. Swap the order and the defaults would overwrite the user's options.
  6. Math.max(...combined) becomes Math.max(1, 2, 3, 4) → 4.
AdvancedOlder way: the arguments object

#Destructuring

Unpack values from arrays and properties from objects into variables.

You write a pattern on the left that mirrors the shape of the data on the right, and JavaScript copies the matching pieces into variables. Arrays are matched by position (using the iterator, so any iterable works); objects are matched by property name, so order doesn't matter. It replaces repetitive lines like const name = user.name; and makes function signatures self-documenting — a parameter like { title, items } tells you exactly what the function reads.

JavaScript
// Arrays — by position
const [first, second, ...rest] = [10, 20, 30, 40];
const [, , third] = [1, 2, 3];        // skip items
let x = 1, y = 2;
[x, y] = [y, x];                      // swap without a temp variable
What’s happening
  1. [first, second, ...rest] = [10, 20, 30, 40] fills by position: first = 10, second = 20, and the rest element gathers the remainder into a new array, rest = [30, 40].
  2. [, , third] — each empty slot skips a position, so third = 3.
  3. The swap: the right side is evaluated first and builds a temporary array [2, 1] from the current values. The pattern then assigns x = 2 and y = 1. No temp variable is needed because the array holds the old values.
  4. The semicolon on the previous line matters. Without it, y = 2 and [x, y] would be glued into y = 2[x, y] — a line starting with [ is a classic missing-semicolon trap.
AdvancedObject destructuring: defaults, rename, nest, rest
JavaScript
// Objects — by name
const user = { id: 7, name: "Rohit", address: { city: "Toronto" } };
const { name, age = 30 } = user;           // default when undefined
const { name: userName } = user;           // rename
const { address: { city } } = user;        // nested
const { id, ...withoutId } = user;         // rest of the properties
What’s happening
  1. { name, age = 30 } reads user.name → "Rohit". user.age doesn't exist, so it's undefined and the default kicks in → age = 30.
  2. { name: userName } reads "take property name, store it in the variable userName" → "Rohit". The colon means rename, not a type annotation.
  3. { address: { city } } goes into user.address and pulls out city → "Toronto". Only city becomes a variable; address does not. If user.address were missing, this would throw, because you can't destructure undefined.
  4. { id, ...withoutId } takes id = 7 and collects the remaining own enumerable properties into a new object, withoutId = { name: "Rohit", address: { city: "Toronto" } }. It's a shallow copy: withoutId.address is the same object as user.address, so changing its city changes user's too.
AdvancedDestructuring function parameters
JavaScript
// In parameters — very common in React
function render({ title, items = [] }) {
  return `${title}: ${items.length}`;
}
render({ title: "Todos" });
What’s happening
  1. The argument { title: "Todos" } is destructured as the function starts: title = "Todos", items is missing → defaults to [].
  2. The body returns "Todos: 0".
  3. Callers pass options by name and in any order, and optional ones get sensible defaults. That's why React components destructure their props this way.
AdvancedEdge case: null and destructuring defaults

Built from Rohit’s “Javascript/HTML interview” study doc. Examples target modern browsers and Node 20+.

RohitDownloads · All chapters

Sync progress across devices

Type the same private phrase on your Mac and your phone, and your Learned ticks follow you between them — on every study site.

The phrase never leaves this device: only a fingerprint of it is sent, and the server stores a fingerprint of that. Anyone who knows the phrase could see or change your ticks, so pick something you don’t use elsewhere.

Sync is on in this browser.

Sync ID

This ID must be the same on every device. If another device shows a different one, its phrase is different (capital letters count): tap “Turn off here” on it and type the phrase again exactly.