JavaScript · Синтаксис · Просунутий рівень

Замикання та область видимості

10 завдань

Лексична область, замикання, приватний стан та патерни замикань

Замикання, лексична область та баг в циклі

#
**Що таке замикання?** Замикання — це функція що *пам'ятає* змінні зі свого зовнішнього scope навіть після того як зовнішня функція повернулась. У JavaScript кожна функція створює замикання над scope де вона була визначена: ```js function makeGreeter(greeting) { // 'greeting' знаходиться в scope makeGreeter return function(name) { // внутрішня функція замикається над 'greeting' — вона досі доступна return `${greeting}, ${name}!`; }; } const sayHello = makeGreeter('Hello'); const sayHi = makeGreeter('Hi'); sayHello('Alice'); // 'Hello, Alice!' — greeting досі 'Hello' sayHi('Bob'); // 'Hi, Bob!' — окреме замикання, greeting це 'Hi' ``` Кожен виклик `makeGreeter` створює *нове* замикання з власним `greeting`. Замикання не спільні — кожне захоплює свою власну копію зовнішнього scope. **Лексична область — scope визначається при написанні, не при виклику** JavaScript використовує *лексичне* (статичне) визначення scope: область видимості функції визначається там де вона *написана* у коді, а не звідки вона *викликається*: ```js const x = 'global'; function outer() { const x = 'outer'; function inner() { console.log(x); // 'outer' — визначено при написанні } return inner; } const fn = outer(); fn(); // 'outer' — навіть якщо викликається з глобального scope ``` **Класичний баг в циклі — `var` vs `let`** `var` має область видимості функції; всі ітерації циклу `for` поділяють один `i`. До того моменту як callbacks спрацьовують, цикл вже завершився і `i` дорівнює кінцевому значенню: ```js // БАГ з var: for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); // логує 3, 3, 3 } // ВИПРАВЛЕННЯ 1: використати let (block-scoped — кожна ітерація отримує власний i): for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); // логує 0, 1, 2 } // ВИПРАВЛЕННЯ 2: IIFE щоб захопити i для кожної ітерації (патерн до ES6): for (var i = 0; i < 3; i++) { (function(j) { setTimeout(() => console.log(j), 0); })(i); } ```

Приватний стан, once та memoize патерни

#
**Замикання для приватного стану — модульний патерн** ```js function makeBankAccount(initial) { let balance = initial; // приватна — не на поверненому об'єкті return { deposit(amount) { 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.balance; // undefined — справді приватна ``` **`once` — виконати рівно один раз** ```js 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('init!'); return 42; }); init(); // 'init!' → 42 init(); // → 42 (без логу) ``` **`memoize` — кешувати результати** ```js 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); }; } ```

partial, debounce, compose/pipe та довідник патернів замикань

#
**`partial` — попередньо заповнити аргументи** ```js function partial(fn, ...presetArgs) { return function(...laterArgs) { return fn(...presetArgs, ...laterArgs); }; } const double = partial((a, b) => a * b, 2); double(5); // 10 ``` **`debounce` — затримати до зупинки викликів** ```js function debounce(fn, delay) { let timerId; return function(...args) { clearTimeout(timerId); timerId = setTimeout(() => fn(...args), delay); }; } ``` **`compose` / `pipe` — ланцюжок функцій** ```js const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x); const process = pipe( s => s.trim(), s => s.toLowerCase(), s => s.replace(/\s+/g, '-'), ); process(' Hello World '); // 'hello-world' ``` **Довідник — патерни замикань** | Патерн | Що замикає | Для чого | |---|---|---| | Лічильник | мутабельний `count` | збереження стану | | Приватний стан | змінні даних | інкапсуляція | | `once` | прапор `called` + `result` | одноразова ініціалізація | | `memoize` | Map кешу | дорогі чисті функції | | `partial` | попередні аргументи | спеціалізація функцій | | `debounce` | `timerId` | обмеження частоти подій |
01

Реалізуйте лічильник на основі замикання

#

Реалізуйте `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
02

Виправте баг замикання в циклі з 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);
}
03

Реалізуйте функцію вищого порядку 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
04

Реалізуйте 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
05

Реалізуйте часткове застосування

#

Реалізуйте `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!'
06

Створіть банківський рахунок з приватним балансом

#

Реалізуйте `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
07

Реалізуйте фабрику 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)
08

Реалізуйте 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'
09

Реалізуйте 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'
10

Передбачте вивід замикань та поясніть

#

Що виведе цей код? Поясніть чому кожне замикання дає свій результат незалежно не заважаючи іншим. ```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.