JavaScript · Синтаксис · Просунутий рівень
Замикання та область видимості
Лексична область, замикання, приватний стан та патерни замикань
Короткий вступ до теми та пояснення перед вправами (вправи нижче):
Приватний стан, once та memoize патерни
#partial, debounce, compose/pipe та довідник патернів замикань
#Вправи:
Реалізуйте лічильник на основі замикання
#Реалізуйте `makeCounter()` що повертає об'єкт з методами `increment()`, `decrement()`, `reset()` та `value()`. Стан лічильника має бути приватним — недоступним безпосередньо на поверненому об'єкті.
function makeCounter() {
// return object with increment, decrement, reset, value
}
Рішення
function makeCounter() {
let count = 0;
return {
increment() { return ++count; },
decrement() { return --count; },
reset() { count = 0; },
value() { return count; },
};
}
const c = makeCounter();
c.increment(); // 1
c.increment(); // 2
c.decrement(); // 1
c.value(); // 1
c.reset();
c.value(); // 0
Виправте баг замикання в циклі з var
#Код нижче логує `3, 3, 3` замість `0, 1, 2`. Поясніть чому, та надайте два виправлення: одне з `let`, одне з IIFE.
// This logs 3, 3, 3 — fix it to log 0, 1, 2
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
Рішення
// Fix 1: use let (each iteration gets its own block-scoped i)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 0, 1, 2
}
// Fix 2: IIFE to capture i per iteration
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => console.log(j), 0);
})(i);
}
Реалізуйте функцію вищого порядку once()
#Реалізуйте `once(fn)` що повертає функцію-обгортку. Обгортка викликає `fn` лише при першому виклику, потім повертає кешований результат без повторного виклику `fn`.
function once(fn) {
// fn should only be called the first time
// subsequent calls return the cached result
}
Рішення
function once(fn) {
let called = false;
let result;
return function(...args) {
if (!called) {
called = true;
result = fn(...args);
}
return result;
};
}
const init = once(() => { console.log('initialising'); return 42; });
init(); // logs 'initialising', returns 42
init(); // returns 42, no log
Реалізуйте memoize()
#Реалізуйте `memoize(fn)` що повертає мемоізовану версію `fn`. При першому виклику з певними аргументами результат обчислюється та кешується. При наступних викликах з тими самими аргументами повертається кешоване значення.
function memoize(fn) {
// cache results keyed by arguments
}
Рішення
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (!cache.has(key)) {
cache.set(key, fn(...args));
}
return cache.get(key);
};
}
const slowSquare = memoize(n => { console.log('computing'); return n * n; });
slowSquare(5); // 'computing' → 25
slowSquare(5); // → 25 (no log, from cache)
slowSquare(6); // 'computing' → 36
Реалізуйте часткове застосування
#Реалізуйте `partial(fn, ...presetArgs)` що повертає нову функцію з попередньо заповненими `presetArgs`. При виклику поверненої функції з додатковими аргументами — вони додаються після preset-аргументів.
function partial(fn, ...presetArgs) {
// return a function with presetArgs pre-filled
}
Рішення
function partial(fn, ...presetArgs) {
return function(...laterArgs) {
return 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
const greet = (greeting, name) => `${greeting}, ${name}!`;
const sayHi = partial(greet, 'Hi');
sayHi('Alice'); // 'Hi, Alice!'
Створіть банківський рахунок з приватним балансом
#Реалізуйте `makeBankAccount(initialBalance)` використовуючи замикання щоб `balance` залишався приватним. Поверніть об'єкт з `deposit(amount)`, `withdraw(amount)` та `getBalance()`. `deposit` має кидати виняток якщо amount ≤ 0; `withdraw` — якщо amount перевищує баланс.
function makeBankAccount(initialBalance) {
// balance must be private
// return { deposit, withdraw, getBalance }
}
Рішення
function makeBankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit(amount) {
if (amount <= 0) throw new Error('Deposit must be positive');
balance += amount;
return balance;
},
withdraw(amount) {
if (amount > balance) throw new Error('Insufficient funds');
balance -= amount;
return balance;
},
getBalance() { return balance; },
};
}
const acc = makeBankAccount(100);
acc.deposit(50); // 150
acc.withdraw(30); // 120
acc.getBalance(); // 120
acc.balance; // undefined — private
Реалізуйте фабрику makeAdder()
#Реалізуйте `makeAdder(n)` що повертає функцію. Повернена функція приймає аргумент `x` і повертає `x + n`. Кожен виклик `makeAdder` має створювати незалежний суматор що пам'ятає власний `n`.
function makeAdder(n) {
// return a function that adds n to its argument
}
Рішення
function makeAdder(n) {
return function(x) {
return x + n;
};
}
const add5 = makeAdder(5);
const add10 = makeAdder(10);
add5(3); // 8
add5(7); // 12
add10(3); // 13
add10(add5(2)); // 17 (add5(2)=7, add10(7)=17)
Реалізуйте debounce()
#Реалізуйте `debounce(fn, delay)` що повертає відкладену версію `fn`. Відкладена функція затримує виклик `fn` поки не пройде `delay` мілісекунд після останнього виклику. Якщо викликається знову до закінчення затримки — таймер скидається.
function debounce(fn, delay) {
// return a debounced version of fn
// fn should only be called delay ms after the last invocation
}
Рішення
function debounce(fn, delay) {
let timerId;
return function(...args) {
clearTimeout(timerId);
timerId = setTimeout(() => fn(...args), delay);
};
}
const handleInput = debounce((value) => {
console.log('searching for:', value);
}, 300);
// Fast typing — only the last call fires after 300ms:
handleInput('h');
handleInput('he');
handleInput('hel');
handleInput('hell');
handleInput('hello');
// After 300ms: 'searching for: hello'
Реалізуйте compose() та pipe()
#Реалізуйте `compose(...fns)` що застосовує функції справа наліво: `compose(f, g, h)(x)` еквівалентно `f(g(h(x)))`. Потім реалізуйте `pipe(...fns)` що робить те саме зліва направо.
// compose(f, g)(x) === f(g(x))
// compose(f, g, h)(x) === f(g(h(x)))
function compose(...fns) {
// right-to-left execution
}
Рішення
function compose(...fns) {
return function(x) {
return fns.reduceRight((v, f) => f(v), x);
};
}
const double = x => x * 2;
const addOne = x => x + 1;
const square = x => x * x;
const transform = compose(double, addOne, square);
// square(3)=9, addOne(9)=10, double(10)=20
transform(3); // 20
// pipe: left-to-right
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const clean = pipe(s => s.trim(), s => s.toLowerCase());
clean(' HELLO '); // 'hello'
Передбачте вивід замикань та поясніть
#Що виведе цей код? Поясніть чому кожне замикання дає свій результат незалежно не заважаючи іншим. ```js function makeMultiplier(x) { return function(y) { return x * y; }; } const triple = makeMultiplier(3); const quadruple = makeMultiplier(4); console.log(triple(5)); console.log(quadruple(5)); console.log(triple(quadruple(2))); ```
function makeMultiplier(x) {
return function(y) {
return x * y;
};
}
const triple = makeMultiplier(3);
const quadruple = makeMultiplier(4);
console.log(triple(5));
console.log(quadruple(5));
console.log(triple(quadruple(2)));
Рішення
// triple(5) → 3 * 5 = 15
// quadruple(5) → 4 * 5 = 20
// quadruple(2) = 8; triple(8) = 3 * 8 = 24
// Output: 15, 20, 24
// Why: each call to makeMultiplier creates a new closure
// capturing its own 'x'. triple closes over x=3,
// quadruple closes over x=4. They don't share state.