Вивчай

Замикання (Closures)

JavaScript Closures замикання — прозора скринька що зберігає конверт всередині захищаючи від зовнішнього доступуJavaScript Closures замикання — прозора скринька що зберігає конверт всередині захищаючи від зовнішнього доступу

Замикання -- одна з найважливіших концепцій JavaScript. Якщо ти розумієш замикання, ти розумієш, як працює JavaScript "під капотом". Добра новина: ти вже використовував замикання, навіть не знаючи про це! Згадай createGreeter з уроку про функції (Block 4, урок 4.6) -- це було замикання.


Лексичне оточення (Lexical Environment)

Коли JavaScript виконує код, кожен блок { } (функція, if, for) створює лексичне оточення -- місце, де зберігаються змінні:

const name = "Олексій"; // глобальне оточення

function greet() {
  const greeting = "Привіт"; // оточення функції greet
  console.log(`${greeting}, ${name}!`);
}

greet(); // "Привіт, Олексій!"

Як JavaScript знайшов name всередині greet? Через scope chain (ланцюжок областей видимості):

  1. Шукає name в оточенні greet -- не знайдено
  2. Піднімається до зовнішнього (глобального) оточення -- знайдено!
Порада

Ключове слово -- лексичне. Це означає, що область видимості визначається тим, де функція написана в коді, а не тим, де вона викликана.


Що таке замикання?

Замикання -- це функція, яка "запам'ятовує" змінні зі свого зовнішнього оточення, навіть після того як зовнішня функція завершилася.

function createCounter() {
  let count = 0; // ця змінна "живе" в замиканні

  return function() {
    count++;
    return count;
  };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

Що тут відбувається:

  1. createCounter() виконалась і повернула внутрішню функцію
  2. Змінна count мала б зникнути (функція завершилась!)
  3. Але внутрішня функція тримає посилання на оточення, де count існує
  4. Тому count залишається "живою" -- це і є замикання

Кожен виклик -- нове замикання

const counter1 = createCounter();
const counter2 = createCounter();

console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 1 (окремий count!)
console.log(counter1()); // 3

Кожен виклик createCounter() створює нове лексичне оточення з власним count.


Фабрики функцій

Замикання ідеально підходять для створення "налаштованих" функцій:

function createGreeter(greeting) {
  return (name) => `${greeting}, ${name}!`;
}

const hello = createGreeter("Привіт");
const goodbye = createGreeter("До побачення");

console.log(hello("Олексій"));   // "Привіт, Олексій!"
console.log(goodbye("Марія"));   // "До побачення, Марія!"
function createMultiplier(factor) {
  return (number) => number * factor;
}

const double = createMultiplier(2);
const triple = createMultiplier(3);
const toPercent = createMultiplier(100);

console.log(double(5));    // 10
console.log(triple(5));    // 15
console.log(toPercent(0.75)); // 75

Приватні змінні

Замикання -- єдиний спосіб створити по-справжньому приватні дані в JavaScript (до появи # в класах):

function createBankAccount(initialBalance) {
  let balance = initialBalance; // приватна змінна

  return {
    deposit(amount) {
      if (amount <= 0) throw new Error("Сума повинна бути додатною");
      balance += amount;
      return balance;
    },
    withdraw(amount) {
      if (amount > balance) throw new Error("Недостатньо коштів");
      balance -= amount;
      return balance;
    },
    getBalance() {
      return balance;
    },
  };
}

const account = createBankAccount(1000);
console.log(account.getBalance()); // 1000
account.deposit(500);
console.log(account.getBalance()); // 1500
account.withdraw(200);
console.log(account.getBalance()); // 1300

// Прямий доступ до balance неможливий:
console.log(account.balance); // undefined
Інфо

Цей патерн називається Module Pattern і був основним способом організації коду до появи ES6 модулів. Навіть зараз він корисний для інкапсуляції стану.


Замикання в циклах -- класична пастка

Це одне з найпопулярніших питань на співбесідах:

// ПРОБЛЕМА: всі функції "бачать" одну й ту саму змінну i
for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000);
}
// Виведе: 3, 3, 3 (а не 0, 1, 2!)

Чому? var не створює нову область видимості для кожної ітерації. Коли setTimeout спрацює, цикл вже завершився і i === 3.

Рішення 1: let (сучасний спосіб)

for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000);
}
// Виведе: 0, 1, 2

let створює нову змінну для кожної ітерації -- кожне замикання отримує своє значення.

Рішення 2: замикання через IIFE (старий спосіб)

for (var i = 0; i < 3; i++) {
  ((j) => {
    setTimeout(() => {
      console.log(j);
    }, 1000);
  })(i);
}
// Виведе: 0, 1, 2
Увага

Завжди використовуй let у циклах. var з замиканнями -- джерело багів. Знати про цю проблему корисно для співбесід, але в реальному коді просто пиши let.


IIFE -- Immediately Invoked Function Expression

Функція, яка виконується одразу після оголошення:

(function() {
  const secret = "Цю змінну ніхто не побачить";
  console.log(secret);
})();

// Або arrow function
(() => {
  const secret = "Теж приватна";
  console.log(secret);
})();

IIFE використовували для створення приватної області видимості до появи let/const та модулів. Зараз це рідкісний патерн, але ти зустрінеш його в старому коді.


Практичні патерни з замиканнями

Мемоїзація -- кешування результатів

function memoize(fn) {
  const cache = {};

  return function(...args) {
    const key = JSON.stringify(args);
    if (key in cache) {
      console.log("З кешу!");
      return cache[key];
    }
    const result = fn(...args);
    cache[key] = result;
    return result;
  };
}

const expensiveCalc = memoize((n) => {
  console.log("Обчислюю...");
  return n * n;
});

console.log(expensiveCalc(5)); // "Обчислюю..." → 25
console.log(expensiveCalc(5)); // "З кешу!" → 25
console.log(expensiveCalc(3)); // "Обчислюю..." → 9

Once -- функція, що виконується лише раз

function once(fn) {
  let called = false;
  let result;

  return function(...args) {
    if (!called) {
      called = true;
      result = fn(...args);
    }
    return result;
  };
}

const initialize = once(() => {
  console.log("Ініціалізація!");
  return { ready: true };
});

console.log(initialize()); // "Ініціалізація!" → { ready: true }
console.log(initialize()); // { ready: true } (без повторного виклику)

Debounce -- затримка виконання

function debounce(fn, delay) {
  let timerId;

  return function(...args) {
    clearTimeout(timerId);
    timerId = setTimeout(() => {
      fn(...args);
    }, delay);
  };
}

// Пошук виконається тільки через 300мс після останнього натискання
const search = debounce((query) => {
  console.log(`Шукаю: ${query}`);
}, 300);

search("J");
search("Ja");
search("Jav");
search("Java");
search("JavaScript"); // Виконається тільки цей виклик
Порада

debounce -- один з найкорисніших патернів у frontend-розробці. Його використовують для пошуку в реальному часі, обробки scroll/resize подій тощо. Ти зустрінеш його в React (Block 8).


Замикання в обробниках подій

Замикання пояснюють, чому обробники подій "бачать" змінні з зовнішнього scope:

function setupButtons() {
  const buttons = document.querySelectorAll(".btn");

  buttons.forEach((button, index) => {
    button.addEventListener("click", () => {
      // Кожен обробник "замкнув" свій index
      console.log(`Натиснуто кнопку ${index}`);
    });
  });
}

Практика: лічильник з API

Створимо лічильник з можливістю збільшення, зменшення та отримання значення:

function createCounter(initial = 0) {
  let value = initial;

  return {
    increment() { return ++value; },
    decrement() { return --value; },
    getValue() { return value; },
    reset() {
      value = initial; // initial теж "замкнута"!
      return value;
    },
  };
}

const counter = createCounter(10);
console.log(counter.increment()); // 11
console.log(counter.increment()); // 12
console.log(counter.decrement()); // 11
console.log(counter.getValue());  // 11
console.log(counter.reset());     // 10

Підсумок

  • Лексичне оточення -- місце, де зберігаються змінні, визначається положенням у коді
  • Замикання -- функція, яка запам'ятовує змінні зовнішнього оточення
  • Кожен виклик зовнішньої функції створює нове замикання
  • Замикання дозволяють створювати приватні змінні та фабрики функцій
  • let у циклах вирішує класичну проблему замикання з var
  • Практичні патерни: memoize, once, debounce

Що далі?

У наступному уроці -- прототипи та класи: як працює наслідування в JavaScript та як писати об'єктно-орієнтований код з ES6 class.

Інфо

Корисні посилання: