Higher-Order Functions & Closures

Closure কোনো জাদু নয় — এটি সংরক্ষিত scope

~40 min Advanced 18 practice problems Live runner

1. Higher-Order Functions (HOF)

An HOF is a function that takes another function as argument, returns a function, or both. The whole array toolkit (map, filter, reduce, forEach) is built on this idea.

hof.js
// Function as argument
const nums = [1, 2, 3, 4];
console.log(nums.map(n => n * 10));   // [10,20,30,40]

// Function as return value
const mult = factor => n => n * factor;
const triple = mult(3);
console.log(triple(5));      // 15

// Both — apply twice
const twice = fn => x => fn(fn(x));
console.log(twice(n => n + 1)(5));  // 7

2. What Is a Closure?

A closure is a function bundled with the lexical scope it was created in. When the outer function returns, its local variables would normally vanish — but if an inner function still references them, they stay alive, locked to that one inner function.

closure-basics.js
function makeCounter() {
    let count = 0;          // captured by the inner fn
    return () => ++count;     // closure
}
const c1 = makeCounter();
const c2 = makeCounter();
console.log(c1(), c1(), c1());   // 1 2 3
console.log(c2());                 // 1 — separate state
Closure মানে — একটি function-এর সাথে তৈরি হওয়ার সময়ের scope-ও সংরক্ষিত থাকে। প্রতিটি makeCounter() call একটি স্বতন্ত্র count তৈরি করে — সেটি কেবল ভেতরের function দেখতে পায়।

3. Private State via Closure

Closure is JavaScript's original encapsulation mechanism — long before # private class fields existed.

private.js
function makeAccount(initial) {
    let balance = initial;
    return {
        deposit(n) { balance += n; },
        withdraw(n) {
            if (n > balance) throw new Error("insufficient");
            balance -= n;
        },
        balance() { return balance; }
    };
}
const a = makeAccount(100);
a.deposit(50);
console.log(a.balance());      // 150
console.log(a.balance);          // undefined (private)

4. The Module Pattern (IIFE)

module.js
const theme = (() => {
    let mode = "light";
    return {
        toggle() { mode = mode === "light" ? "dark" : "light"; },
        get() { return mode; }
    };
})();

theme.toggle();
console.log(theme.get());  // "dark"

5. The Famous Interview Question

What does this print?

classic.js
for (var i = 0; i < 3; i++) {
    setTimeout(() => console.log("var:", i), 10);
}

for (let j = 0; j < 3; j++) {
    setTimeout(() => console.log("let:", j), 10);
}

The first loop logs 3 3 3 — every callback closes over the same i binding. The second loop logs 0 1 2 — let creates a fresh binding each iteration. This is the single best argument against var.

6. Memoization with a Closure

memo.js
function memoize(fn) {
    const cache = new Map();
    return function (x) {
        if (!cache.has(x)) cache.set(x, fn(x));
        return cache.get(x);
    };
}

function slowFib(n) {
    if (n < 2) return n;
    return slowFib(n - 1) + slowFib(n - 2);
}
const fastFib = memoize(slowFib);
console.log(fastFib(30));   // computed once, cached
console.log(fastFib(30));   // instant

7. Closure Scope Chain — Visualized

Global scope const x = 100 makeCounter() — outer scope let count = 0 ← captured return () => ++count ← the closure Figure 12.1 — inner function keeps a live reference to count long after makeCounter has returned.

8. Glossary (শব্দকোষ)

TermMeaningবাংলায়
HOFHigher-Order Function — takes or returns a function.Higher-Order Function — function নেয় বা ফেরত দেয়।
ClosureInner function + the lexical scope it was created in.Inner function-এর সাথে তৈরির সময়ের scope সংরক্ষিত থাকা।
CaptureThe inner function "remembers" outer variables by reference.Inner function বাইরের variable-কে reference হিসেবে ধরে রাখে।
Module patternIIFE that returns an object with private state via closure.IIFE যা closure দিয়ে private state রেখে object return করে।
MemoizationCaching function results by their input.Input অনুযায়ী result cache করে রাখা।
CurryingTransforming f(a, b) into f(a)(b).f(a, b) কে f(a)(b) তে রূপান্তর।
Partial applicationPre-fixing some arguments of a function.Function-এর কিছু argument আগে থেকেই দেওয়া।
Compose / pipeCombine functions to flow data through them.Function-গুলোকে যুক্ত করে data এক প্রবাহে চালানো।
মূল ধারণা: Closure কোনো জাদু নয় — এটি শুধু সংরক্ষিত scope। প্রতিটি makeCounter() call একটি স্বতন্ত্র count তৈরি করে; ভেতরের function সেটি দেখতে পায়। HOF আর closure মিলে memoize, debounce, throttle, partial, module pattern — আধুনিক JS-এর অনেক pattern সম্ভব করে।

9. Practice Problems

  1. Write makeAdder(x) that returns a function adding x to its argument.
    ✨ Show Answer
    a1.js
    const makeAdder = x => y => x + y;
    const add5 = makeAdder(5);
    console.log(add5(3));     // 8
  2. Build a counter that supports increment, decrement, and reset.
    ✨ Show Answer
    a2.js
    function makeCounter(start = 0) {
        let n = start;
        return {
            inc() { return ++n; },
            dec() { return --n; },
            reset() { n = start; return n; }
        };
    }
    const c = makeCounter(10);
    console.log(c.inc(), c.inc(), c.dec(), c.reset());
  3. Implement compose(f, g)(x) === f(g(x)).
    ✨ Show Answer
    a3.js
    const compose = (f, g) => x => f(g(x));
    const pipeline = compose(n => n * 2, n => n + 1);
    console.log(pipeline(5)); // (5+1)*2 = 12
  4. Implement curry for a 3-arg function.
    ✨ Show Answer
    a4.js
    const curry = fn => a => b => c => fn(a, b, c);
    const add3 = (x, y, z) => x + y + z;
    console.log(curry(add3)(1)(2)(3)); // 6
  5. Show the var-loop bug, then fix it three ways: let, IIFE, and forEach.
    ✨ Show Answer
    a5.js
    // Bug
    for (var i = 0; i < 3; i++)
        setTimeout(() => console.log("bug", i), 0);
    
    // Fix 1: let
    for (let j = 0; j < 3; j++)
        setTimeout(() => console.log("let", j), 0);
    
    // Fix 2: IIFE
    for (var k = 0; k < 3; k++)
        ((idx) => setTimeout(() => console.log("iife", idx), 0))(k);
    
    // Fix 3: forEach
    [0,1,2].forEach(idx =>
        setTimeout(() => console.log("each", idx), 0));
  6. Build a private "secret" holder where the secret can only be read with a correct password.
    ✨ Show Answer
    a6.js
    function vault(secret, pwd) {
        return attempt => attempt === pwd ? secret : "denied";
    }
    const v = vault("42 is the answer", "hello");
    console.log(v("x"));
    console.log(v("hello"));
  7. Implement partial(fn, ...preset) — like .bind without changing this.
    ✨ Show Answer
    a7.js
    const partial = (fn, ...preset) => (...rest) => fn(...preset, ...rest);
    const greet = (greeting, name) => `${greeting}, ${name}!`;
    const hi = partial(greet, "Hi");
    console.log(hi("Arif"));
  8. Build throttle(fn, ms): ignore subsequent calls within ms.
    ✨ Show Answer
    a8.js
    const throttle = (fn, ms) => {
        let last = 0;
        return (...args) => {
            const now = Date.now();
            if (now - last >= ms) {
                last = now;
                return fn(...args);
            }
        };
    };
    const log = throttle(msg => console.log("hit:", msg), 100);
    log("a"); log("b"); log("c"); // only "a" within 100 ms
  9. Build debounce(fn, ms): only fire after ms of silence.
    ✨ Show Answer
    a9.js
    const debounce = (fn, ms) => {
        let id;
        return (...args) => {
            clearTimeout(id);
            id = setTimeout(() => fn(...args), ms);
        };
    };
    const save = debounce(v => console.log("saved:", v), 200);
    save("a"); save("ab"); save("abc"); // only "abc"
  10. Closure-based id generator: each call returns id 1, 2, 3...
    ✨ Show Answer
    a10.js
    const nextId = (() => { let n = 0; return () => ++n; })();
    console.log(nextId(), nextId(), nextId());
  11. Closure with multiple variables: write a "stopwatch" with start, stop, lap.
    ✨ Show Answer
    a11.js
    function stopwatch() {
        let t0 = null;
        return {
            start() { t0 = Date.now(); },
            lap() { return Date.now() - t0; }
        };
    }
    const sw = stopwatch();
    sw.start();
    for (let i = 0; i < 1e6; i++);
    console.log("ms:", sw.lap());
  12. Use a closure to track the number of times a function has been called.
    ✨ Show Answer
    a12.js
    const spy = fn => {
        let calls = 0;
        const wrapped = (...args) => { calls++; return fn(...args); };
        wrapped.calls = () => calls;
        return wrapped;
    };
    const add = spy((a, b) => a + b);
    add(1, 2); add(3, 4);
    console.log(add.calls()); // 2
  13. Build pipe(...fns) that returns a function applying them left-to-right.
    ✨ Show Answer
    a13.js
    const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
    const p = pipe(n => n + 1, n => n * 2, n => n - 3);
    console.log(p(5)); // ((5+1)*2)-3 = 9
  14. Why is the closure pattern slower than a class with private # fields?
    ✨ Show Answer

    Answer: Each call to a "factory" function creates fresh closures and fresh method functions, costing memory and a tiny bit of allocation. A class shares one method definition on its prototype across all instances. For tiny utilities the difference is invisible; for hot paths producing thousands of instances, classes win.

  15. Show that closures over a single variable share state, not copies.
    ✨ Show Answer
    a15.js
    function pair() {
        let n = 0;
        return [() => ++n, () => n];
    }
    const [inc, peek] = pair();
    inc(); inc(); inc();
    console.log(peek()); // 3 — shared
  16. Use a closure to make a once-per-event handler.
    ✨ Show Answer
    a16.js
    const once = fn => {
        let fired = false;
        return (...a) => { if (!fired) { fired = true; return fn(...a); } };
    };
    const handler = once(() => console.log("clicked"));
    handler(); handler(); handler();
  17. In one paragraph, explain a closure to a friend who knows Python.
    ✨ Show Answer

    Answer: A closure is a function plus the local variables it remembers from where it was defined. Python has the same idea — a nested function can refer to a variable from its enclosing function and that variable stays alive even after the outer function returns. JavaScript's let/const simply make this work cleanly per scope, with no extra nonlocal keyword required to mutate; you just close over the binding.

  18. Build a tiny event bus: on(name, fn), emit(name, data).
    ✨ Show Answer
    a18.js
    const bus = (() => {
        const map = {};
        return {
            on(name, fn) { (map[name] ??= []).push(fn); },
            emit(name, data) { (map[name] || []).forEach(f => f(data)); }
        };
    })();
    bus.on("hi", d => console.log("got:", d));
    bus.emit("hi", "hello");

Summary — Module 12

Higher-order functions accept or return functions — they're the foundation of declarative array work. Closures are simply inner functions that keep their outer scope alive. Together, they enable counters, factories, memoization, debounce/throttle, and the module pattern. The classic var+setTimeout bug is a closure being asked to remember one shared binding instead of three different ones.

HOF function নেয় বা ফেরত দেয়। Closure মানে — function-এর সাথে তার scope সংরক্ষিত। দুই concept মিলে JS-এর কার্যকরী style সম্ভব হয়।

Next Module → Objects — properties, methods, this।