JavaScript · Syntax · Intermediate

Closures

10 tasks

Functions that remember their outer scope. Covers closure mechanics and practical patterns like counters and factories.

What is a closure: scope chain, private state, shared bindings

#
A closure is a function that *remembers* the variables from the scope where it was defined, even after that outer scope has finished executing. ```javascript function makeCounter() { let count = 0; // outer variable return function () { // inner function — a closure count += 1; return count; }; } const counter = makeCounter(); counter(); // 1 counter(); // 2 counter(); // 3 ``` After `makeCounter()` returns, you'd expect `count` to be gone. But `counter` still holds a reference to it — that's the closure. **The scope chain** When a function looks up a variable, it walks up the scope chain: local scope → enclosing function scope → module scope → global scope. A closure captures the enclosing scope at the time the inner function is *defined*, not called. ```javascript const x = 'global'; function outer() { const x = 'outer'; function inner() { console.log(x); // 'outer' — captured from enclosing scope } return inner; } const fn = outer(); fn(); // 'outer' (not 'global') ``` **Closure vs global variable** Both store state that persists across calls. The difference: a closure keeps state *private* to the function that created it, while a global variable is shared by everything. ```javascript // Global state - anyone can accidentally modify it let globalCount = 0; function incrementGlobal() { globalCount++; return globalCount; } // Closure state - only the returned function can change it function makeSecureCounter() { let count = 0; return () => ++count; } const secureCount = makeSecureCounter(); // count is not accessible from outside ``` **Multiple closures sharing the same outer variable** Two closures created in the same scope share the *same* binding, not independent copies: ```javascript function makeUpDown() { let n = 0; return { up: () => ++n, down: () => --n, get: () => n, }; } const counter = makeUpDown(); counter.up(); // 1 counter.up(); // 2 counter.down(); // 1 counter.get(); // 1 — both up and down see the same n ```

Factory functions, the var-in-loop bug, and the module pattern

#
**Factory functions** A factory is a function that creates and returns other functions. Each returned function closes over its own private state: ```javascript function makeAdder(x) { return (y) => x + y; // x is captured } const add5 = makeAdder(5); const add10 = makeAdder(10); add5(3) // 8 add10(3) // 13 — independent from add5, has its own x ``` **The classic `var`-in-loop bug** This is one of the most famous JavaScript gotchas. All closures in the loop share the *same* `var i` binding, which ends up as 3 after the loop finishes: ```javascript const fns = []; for (var i = 0; i < 3; i++) { fns.push(() => i); // all close over the same 'i' } fns[0](); // 3 (not 0!) fns[1](); // 3 fns[2](); // 3 ``` **Fix 1: use `let`** — `let` creates a new binding per iteration: ```javascript const fns = []; for (let i = 0; i < 3; i++) { fns.push(() => i); // each iteration has its own 'i' } fns[0](); // 0 fns[1](); // 1 fns[2](); // 2 ``` **Fix 2: IIFE** (immediately invoked function expression) — captures the value at that moment: ```javascript const fns = []; for (var i = 0; i < 3; i++) { fns.push(((captured) => () => captured)(i)); } fns[0](); // 0 ``` The IIFE solution is historical — before `let` existed (ES6). Today, always use `let` (or `const`) in loops. **The module pattern** Before ES6 modules, closures were used to create private state: an IIFE returns an object with public methods while keeping implementation details hidden. ```javascript const bankAccount = (() => { let balance = 0; // private return { deposit: (n) => { balance += n; }, withdraw: (n) => { balance = Math.max(0, balance - n); }, getBalance: () => balance, }; })(); bankAccount.deposit(100); bankAccount.withdraw(30); bankAccount.getBalance(); // 70 // balance is not accessible directly ```

Closure patterns: memoize, once, partial, and memory pitfalls

#
**Memoize** — cache the result of expensive calls: ```javascript function memoize(fn) { const cache = new Map(); return function (...args) { const key = JSON.stringify(args); if (cache.has(key)) return cache.get(key); const result = fn.apply(this, args); cache.set(key, result); return result; }; } const fib = memoize(function (n) { if (n <= 1) return n; return fib(n - 1) + fib(n - 2); }); fib(40); // instant ``` **Once** — run a function only on the first call: ```javascript function once(fn) { let called = false; let result; return function (...args) { if (!called) { called = true; result = fn.apply(this, args); } return result; }; } const initDB = once(() => { console.log('DB connected'); return 'ok'; }); initDB(); // 'DB connected' -> 'ok' initDB(); // (silent) -> 'ok' — same result, no side effects ``` **Partial application** — pre-fill some arguments: ```javascript function partial(fn, ...presetArgs) { return (...laterArgs) => fn(...presetArgs, ...laterArgs); } const multiply = (a, b) => a * b; const double = partial(multiply, 2); const triple = partial(multiply, 3); double(5); // 10 triple(5); // 15 ``` **Closure pitfall: unintended memory retention** Closures keep the entire outer scope alive. If the outer scope holds large objects, they won't be garbage collected: ```javascript function processLargeData(data) { const hugeArray = new Array(1_000_000).fill(data); // large return () => hugeArray.length; // closure keeps hugeArray alive forever } // Fix: only capture what you need function processLargeData(data) { const hugeArray = new Array(1_000_000).fill(data); const size = hugeArray.length; // extract the value return () => size; // closure only captures 'size', not hugeArray } ``` **setTimeout with closures** Callbacks passed to `setTimeout` close over the variables at the time the callback is created: ```javascript for (let i = 1; i <= 3; i++) { setTimeout(() => console.log(i), i * 1000); // prints 1, 2, 3 (with let) } // With var: would print 4, 4, 4 ```
01

Counter factory

#

Write a function makeCounter() that returns a new counter object. Each call to makeCounter() should create an independent counter with increment() and getValue() methods.

function makeCounter() {

}

const counter = makeCounter();
counter.increment();
counter.increment();
counter.increment();
console.log(counter.getValue());
Solution
function makeCounter() {
    let count = 0;
    return {
        increment() { count++; },
        getValue() { return count; },
    };
}

const counter = makeCounter();
counter.increment();
counter.increment();
counter.increment();
console.log(counter.getValue());
02

Multiplier factory

#

Write a function makeMultiplier(factor) that returns a new function. That returned function takes a number and multiplies it by the original factor.

function makeMultiplier(factor) {

}

const triple = makeMultiplier(3);
const double = makeMultiplier(2);
console.log(triple(5));
console.log(double(7));
Solution
function makeMultiplier(factor) {
    return function(n) {
        return n * factor;
    };
}

const triple = makeMultiplier(3);
const double = makeMultiplier(2);
console.log(triple(5));
console.log(double(7));
03

Once

#

Write a function once(fn) that takes a function and returns a new function that can only be called once. On the first call it executes fn and returns the result; subsequent calls return undefined.

function once(fn) {

}

const init = once(() => {
    console.log("initialized!");
    return 42;
});
console.log(init());
console.log(init());
console.log(init());
Solution
function once(fn) {
    let called = false;
    return function(...args) {
        if (!called) {
            called = true;
            return fn(...args);
        }
    };
}

const init = once(() => {
    console.log("initialized!");
    return 42;
});
console.log(init());
console.log(init());
console.log(init());
04

Adder factory

#

Write a function makeAdder(x) that returns a function. The returned function takes a number y and returns x + y.

function makeAdder(x) {

}

const add5 = makeAdder(5);
const add10 = makeAdder(10);
console.log(add5(3));
console.log(add10(3));
Solution
function makeAdder(x) {
    return y => x + y;
}

const add5 = makeAdder(5);
const add10 = makeAdder(10);
console.log(add5(3));
console.log(add10(3));
05

Memoize

#

Write a function memoize(fn) that wraps a function and caches its results. If the function is called again with the same argument, return the cached result instead of calling fn again.

function memoize(fn) {

}

const slowSquare = memoize(n => {
    console.log(`computing ${n}...`);
    return n * n;
});
console.log(slowSquare(4));
console.log(slowSquare(4));
console.log(slowSquare(5));
Solution
function memoize(fn) {
    const cache = {};
    return function(arg) {
        if (arg in cache) return cache[arg];
        cache[arg] = fn(arg);
        return cache[arg];
    };
}

const slowSquare = memoize(n => {
    console.log(`computing ${n}...`);
    return n * n;
});
console.log(slowSquare(4));
console.log(slowSquare(4));
console.log(slowSquare(5));
06

Private variable

#

Write a function makeBankAccount(initialBalance) that returns an object with deposit(amount) and withdraw(amount) methods and a getBalance() method. The balance should not be directly accessible from outside.

function makeBankAccount(initialBalance) {

}

const account = makeBankAccount(100);
account.deposit(50);
account.withdraw(30);
console.log(account.getBalance());
Solution
function makeBankAccount(initialBalance) {
    let balance = initialBalance;
    return {
        deposit(amount) { balance += amount; },
        withdraw(amount) { balance -= amount; },
        getBalance() { return balance; },
    };
}

const account = makeBankAccount(100);
account.deposit(50);
account.withdraw(30);
console.log(account.getBalance());
07

Delayed logger

#

Write a function delayedLog(message, seconds) that logs the message to the console after the given number of seconds. Use setTimeout and a closure.

function delayedLog(message, seconds) {

}

delayedLog("hello", 1);
delayedLog("world", 2);
Solution
function delayedLog(message, seconds) {
    setTimeout(() => {
        console.log(message);
    }, seconds * 1000);
}

delayedLog("hello", 1);
delayedLog("world", 2);
08

Clamp factory

#

Write a function makeClamp(min, max) that returns a function. The returned function takes a number and clamps it between min and max (inclusive).

function makeClamp(min, max) {

}

const clamp0to100 = makeClamp(0, 100);
console.log(clamp0to100(-5));
console.log(clamp0to100(50));
console.log(clamp0to100(150));
Solution
function makeClamp(min, max) {
    return n => Math.min(Math.max(n, min), max);
}

const clamp0to100 = makeClamp(0, 100);
console.log(clamp0to100(-5));
console.log(clamp0to100(50));
console.log(clamp0to100(150));
09

Call count wrapper

#

Write a function withCallCount(fn) that wraps a function and returns an object with a call method and a getCount() method. call() invokes the original function and getCount() returns how many times it was called.

function withCallCount(fn) {

}

const wrapped = withCallCount(x => x * 2);
wrapped.call(5);
wrapped.call(10);
wrapped.call(3);
console.log(wrapped.getCount());
Solution
function withCallCount(fn) {
    let count = 0;
    return {
        call(...args) {
            count++;
            return fn(...args);
        },
        getCount() { return count; },
    };
}

const wrapped = withCallCount(x => x * 2);
wrapped.call(5);
wrapped.call(10);
wrapped.call(3);
console.log(wrapped.getCount());
10

Partial application

#

Write a function partial(fn, ...preArgs) that takes a function and some arguments, and returns a new function that prepends those arguments to any future arguments when calling fn.

function partial(fn, ...preArgs) {

}

function add(a, b, c) {
    return a + b + c;
}

const add10 = partial(add, 10);
console.log(add10(5, 3));
console.log(add10(1, 2));
Solution
function partial(fn, ...preArgs) {
    return function(...laterArgs) {
        return fn(...preArgs, ...laterArgs);
    };
}

function add(a, b, c) {
    return a + b + c;
}

const add10 = partial(add, 10);
console.log(add10(5, 3));
console.log(add10(1, 2));