JavaScript · Синтаксис · Промежуточный

Замыкания

10 задач

Функции, которые помнят свою внешнюю область видимости. Охватывает механику замыканий и практические паттерны — счётчики и фабрики.

Что такое замыкание: цепочка областей, приватное состояние, общие привязывания

#
Замыкание — это функция, которая *помнит* переменные из области видимости, где она была определена, даже после того, как эта внешняя область завершила выполнение. ```javascript function makeCounter() { let count = 0; // внешняя переменная return function () { // внутренняя функция — замыкание count += 1; return count; }; } const counter = makeCounter(); counter(); // 1 counter(); // 2 counter(); // 3 ``` После возврата `makeCounter()` можно было бы ожидать, что `count` исчезнет. Но `counter` всё ещё хранит ссылку на него — это и есть замыкание. **Цепочка областей видимости** Когда функция ищет переменную, она проходит по цепочке областей видимости: локальная → охватывающая функция → область модуля → глобальная. Замыкание захватывает внешнюю область в момент *определения* внутренней функции, а не вызова. ```javascript const x = 'global'; function outer() { const x = 'outer'; function inner() { console.log(x); // 'outer' — захвачено из охватывающей области } return inner; } const fn = outer(); fn(); // 'outer' (не 'global') ``` **Замыкание vs глобальная переменная** Оба хранят состояние между вызовами. Разница: замыкание держит состояние *приватным* для функции, которая его создала, тогда как глобальная переменная доступна всем. ```javascript // Глобальное состояние - кто угодно может случайно его изменить let globalCount = 0; function incrementGlobal() { globalCount++; return globalCount; } // Состояние замыкания - только возвращённая функция может его изменить function makeSecureCounter() { let count = 0; return () => ++count; } const secureCount = makeSecureCounter(); // count недоступен снаружи ``` **Несколько замыканий, делящих одну и ту же внешнюю переменную** Два замыкания, созданные в одной области видимости, делят *одно* привязывание, а не независимые копии: ```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 — и up, и down видят одно и то же n ```

Функции-фабрики, ошибка var в цикле и паттерн модуля

#
**Функции-фабрики** Фабрика — это функция, которая создаёт и возвращает другие функции. Каждая возвращённая функция замыкается на собственном приватном состоянии: ```javascript function makeAdder(x) { return (y) => x + y; // x захвачено } const add5 = makeAdder(5); const add10 = makeAdder(10); add5(3) // 8 add10(3) // 13 — независим от add5, имеет собственный x ``` **Классическая ошибка с `var` в цикле** Это одна из самых известных ловушек JavaScript. Все замыкания в цикле делят *одно* привязывание `var i`, которое после завершения цикла равно 3: ```javascript const fns = []; for (var i = 0; i < 3; i++) { fns.push(() => i); // все замыкаются на одном 'i' } fns[0](); // 3 (не 0!) fns[1](); // 3 fns[2](); // 3 ``` **Исправление 1: использовать `let`** — `let` создаёт новое привязывание для каждой итерации: ```javascript const fns = []; for (let i = 0; i < 3; i++) { fns.push(() => i); // каждая итерация имеет собственный 'i' } fns[0](); // 0 fns[1](); // 1 fns[2](); // 2 ``` **Исправление 2: IIFE** (немедленно вызываемая функция) — захватывает значение в тот момент: ```javascript const fns = []; for (var i = 0; i < 3; i++) { fns.push(((captured) => () => captured)(i)); } fns[0](); // 0 ``` Решение с IIFE — историческое, до появления `let` (ES6). Сегодня всегда используй `let` или `const` в циклах. **Паттерн модуля** До модулей ES6 замыкания использовались для приватного состояния: IIFE возвращает объект с публичными методами, скрывая детали реализации. ```javascript const bankAccount = (() => { let balance = 0; // приватное 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 не доступен напрямую ```

Паттерны замыканий: мемоизация, once, partial и ловушки памяти

#
**Мемоизация** — кешировать результат дорогих вызовов: ```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); // мгновенно ``` **Once** — выполнить функцию только при первом вызове: ```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 подключена'); return 'ok'; }); initDB(); // 'DB подключена' -> 'ok' initDB(); // (тихо) -> 'ok' — тот же результат, без побочных эффектов ``` **Частичное применение** — заранее заполнить некоторые аргументы: ```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 ``` **Ловушка замыканий: непреднамеренное удержание памяти** Замыкания держат всю внешнюю область живой. Если внешняя область содержит большие объекты, сборщик мусора не сможет их удалить: ```javascript function processLargeData(data) { const hugeArray = new Array(1_000_000).fill(data); // большой return () => hugeArray.length; // замыкание держит hugeArray живым навсегда } // Исправление: захватывай только то, что нужно function processLargeData(data) { const hugeArray = new Array(1_000_000).fill(data); const size = hugeArray.length; // извлечь значение return () => size; // замыкание захватывает только 'size', не hugeArray } ``` **setTimeout с замыканиями** Обратные вызовы, переданные в `setTimeout`, замыкаются на переменных в момент их создания: ```javascript for (let i = 1; i <= 3; i++) { setTimeout(() => console.log(i), i * 1000); // выводит 1, 2, 3 (с let) } // С var: вывело бы 4, 4, 4 ```
01

Фабрика счётчиков

#

Напишите функцию makeCounter(), которая возвращает новый объект-счётчик. Каждый вызов makeCounter() должен создавать независимый счётчик с методами increment() и getValue().

function makeCounter() {

}

const counter = makeCounter();
counter.increment();
counter.increment();
counter.increment();
console.log(counter.getValue());
Решение
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

Фабрика множителей

#

Напишите функцию makeMultiplier(factor), которая возвращает новую функцию. Эта возвращённая функция принимает число и умножает его на исходный factor.

function makeMultiplier(factor) {

}

const triple = makeMultiplier(3);
const double = makeMultiplier(2);
console.log(triple(5));
console.log(double(7));
Решение
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(fn), которая принимает функцию и возвращает новую функцию, которую можно вызвать только один раз. При первом вызове она выполняет fn и возвращает результат; последующие вызовы возвращают undefined.

function once(fn) {

}

const init = once(() => {
    console.log("initialized!");
    return 42;
});
console.log(init());
console.log(init());
console.log(init());
Решение
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

Фабрика сумматоров

#

Напишите функцию makeAdder(x), которая возвращает функцию. Возвращённая функция принимает число y и возвращает x + y.

function makeAdder(x) {

}

const add5 = makeAdder(5);
const add10 = makeAdder(10);
console.log(add5(3));
console.log(add10(3));
Решение
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(fn), которая оборачивает функцию и кэширует её результаты. Если функцию снова вызывают с тем же аргументом, верните кэшированный результат вместо повторного вызова fn.

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));
Решение
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

Приватная переменная

#

Напишите функцию makeBankAccount(initialBalance), которая возвращает объект с методами deposit(amount), withdraw(amount) и getBalance(). Баланс не должен быть доступен снаружи напрямую.

function makeBankAccount(initialBalance) {

}

const account = makeBankAccount(100);
account.deposit(50);
account.withdraw(30);
console.log(account.getBalance());
Решение
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

Отложенный лог

#

Напишите функцию delayedLog(message, seconds), которая выводит сообщение в консоль через заданное количество секунд. Используйте setTimeout и замыкание.

function delayedLog(message, seconds) {

}

delayedLog("hello", 1);
delayedLog("world", 2);
Решение
function delayedLog(message, seconds) {
    setTimeout(() => {
        console.log(message);
    }, seconds * 1000);
}

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

Фабрика ограничений

#

Напишите функцию makeClamp(min, max), которая возвращает функцию. Возвращённая функция принимает число и ограничивает его между min и max включительно.

function makeClamp(min, max) {

}

const clamp0to100 = makeClamp(0, 100);
console.log(clamp0to100(-5));
console.log(clamp0to100(50));
console.log(clamp0to100(150));
Решение
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

Обёртка со счётчиком вызовов

#

Напишите функцию withCallCount(fn), которая оборачивает функцию и возвращает объект с методом call и методом getCount(). call() вызывает исходную функцию, а getCount() возвращает количество вызовов.

function withCallCount(fn) {

}

const wrapped = withCallCount(x => x * 2);
wrapped.call(5);
wrapped.call(10);
wrapped.call(3);
console.log(wrapped.getCount());
Решение
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(fn, ...preArgs), которая принимает функцию и некоторые аргументы и возвращает новую функцию, которая при вызове подставляет эти аргументы первыми в 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));
Решение
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));