Skip to main content

Замикання

Замикання (closure) це функція, яка «пам'ятає» змінні з області, де її було створено, навіть якщо ця область уже завершила виконання. Інакше кажучи, замикання це функція разом із лексичним оточенням, у якому вона народилася.

Теорія

TL;DR

  • Замикання виникає, коли функція оголошена всередині іншої функції й використовує її змінні.
  • Зовнішня функція може завершитися, але захоплені змінні не знищуються, поки на них є посилання.
  • Замикання зберігає посилання на змінну, а не її копію, тому стан живе між викликами.
  • Кожен виклик зовнішньої функції створює нове незалежне оточення зі своїми змінними.
  • Це основний спосіб робити приватні дані, лічильники, фабрики функцій і мемоізацію.
  • Класична пастка: var у циклі разом із setTimeout дає одну спільну змінну на всі ітерації.

Швидкий приклад

javascript
function outer() { let count = 0; // змінна зовнішньої функції function inner() { count++; console.log(count); } return inner; } const counter = outer(); // викликаємо зовнішню функцію counter(); // 1 counter(); // 2 counter(); // 3

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

  1. outer() виконується і створює змінну count та внутрішню функцію inner.
  2. Повертає inner, яка пам'ятає, де її було створено.
  3. Навіть після завершення outer() змінна count не знищується, бо на неї є посилання з inner.

Коротко ланцюжок такий:

text
outer() -> створило count -> inner() -> запам'ятало count

Замикання це функція плюс її оточення

Кожне замикання зберігає лексичне оточення (scope), тобто набір змінних, до яких воно має доступ.

javascript
function makeAdder(x) { return function (y) { return x + y; // доступ до "x" із зовнішнього скоупу }; } const add5 = makeAdder(5); console.log(add5(10)); // 15 console.log(add5(7)); // 12

Пояснення:

  • Функція makeAdder створює і повертає нову функцію.
  • Повернена функція замикає значення x (у нашому випадку 5).
  • Навіть після завершення makeAdder змінна x залишається «живою».

Важливо, що makeAdder(5) і makeAdder(10) дадуть дві різні функції з двома різними оточеннями, які нічого не знають одна про одну.

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

Замикання це штатний спосіб зробити дані недоступними ззовні.

javascript
function createCounter() { let count = 0; return { increment() { count++; }, decrement() { count--; }, get() { return count; } }; } const counter = createCounter(); counter.increment(); counter.increment(); console.log(counter.get()); // 2

Тут count недоступний ззовні напряму, але доступний із замкнених функцій (increment, decrement, get). Це і є спосіб створювати приватні змінні у JavaScript.

Замикання зберігає посилання, а не значення

Функція пам'ятає не значення, а посилання на змінну в зовнішньому оточенні.

javascript
function make() { let value = 1; return () => console.log(value++); } const f = make(); f(); // 1 f(); // 2, змінна "value" жива між викликами

Саме тому лічильник працює: обидва виклики читають і змінюють ту саму комірку пам'яті.

Класична помилка з циклом

javascript
for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 1000); } // через 1 секунду: 3, 3, 3

Чому так:

  • Усі функції замкнули одну й ту саму змінну i, оголошену через var у функціональній області видимості.
  • Коли спрацьовує таймер, цикл уже завершився і i дорівнює 3.

Виправлення через let, який створює нову змінну на кожній ітерації:

javascript
for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 1000); } // 0, 1, 2

Підсумок і аналогія

Що цеПриклад
Функція, яка «пам'ятає» змінні з місця, де її створеноreturn function () { console.log(a); }
Змінні живуть, навіть якщо зовнішня функція вже завершиласяТак
Створює приватні дані та станТак
Часто застосовується в колбеках, обробниках подій, модуляхТак

Замикання це як рюкзак, у якому функція несе з собою всі потрібні змінні з місця, де вона народилася.

Типові помилки

  • Думати, що замикання копіює значення. Воно тримає посилання на змінну, тож пізніша зміна цієї змінної буде видима всередині.
  • Використовувати var у циклі з асинхронним колбеком. Отримаєте останнє значення лічильника на всіх ітераціях. Беріть let або обгортайте тіло у функцію.
  • Вважати, що кожен виклик зовнішньої функції ділить один стан. Навпаки: кожен виклик створює окреме оточення, тому два лічильники незалежні.
  • Створювати замикання в циклах на тисячі елементів без потреби. Кожне тримає своє оточення в пам'яті, і якщо посилання не відпустити, дані не зберуться збирачем сміття, а це витік пам'яті.
  • Плутати замикання з this. Замикання захоплює змінні, а не this: у звичайній функції this визначається способом виклику, і лише стрілкова функція бере this із зовнішнього оточення.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.