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));