Skip to main content

Користь від closures

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

Теорія

TL;DR

  • Closure, це пара «функція + лексичне оточення, у якому її створено».
  • Внутрішня функція тримає посилання на оточення зовнішньої, тому змінні не зникають після її завершення.
  • Головні застосування: приватні дані, фабрики функцій, стан між викликами, мемоізація, currying.
  • Замикання створюється автоматично, робити для цього нічого спеціального не треба.
  • Closure зберігає посилання на змінну, а не її копію на момент створення.
  • Зайві закриті посилання тримають пам'ять, тож великі об'єкти в закритому оточенні, це реальний ризик витоку.

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

javascript
function outer() { let count = 0; function inner() { count++; console.log(count); } return inner; } const counter = outer(); counter(); // 1 counter(); // 2 counter(); // 3

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

  • outer() повертає inner;
  • inner запам'ятала змінну count зі свого лексичного оточення;
  • навіть після завершення outer() змінна count не зникає;
  • це і є closure, «збережена пам'ять» функції.

Приватні дані та інкапсуляція

Змінні зовнішньої функції недосяжні ззовні, але доступні всім функціям, що в ній створені. Це дає приватний стан без класів і без #-полів:

javascript
function createUser(name) { let score = 0; return { getName() { return name; }, addScore() { score++; }, getScore() { return score; } }; } const user = createUser('Oleh'); user.addScore(); user.addScore(); console.log(user.getName(), user.getScore()); // Oleh 2

Тут score, це приватна змінна: user.score не існує, прочитати чи зіпсувати її ззовні неможливо, але addScore і getScore мають до неї повний доступ. Так само будують module pattern: назовні віддають лише публічний API, решта лишається у закритому оточенні.

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

Closure дозволяє «вшити» конфігурацію у функцію на момент створення:

javascript
function makeMultiplier(factor) { return function(num) { return num * factor; }; } const double = makeMultiplier(2); const triple = makeMultiplier(3); console.log(double(5)); // 10 console.log(triple(5)); // 15

Кожна створена функція має власне значення factor і пам'ятає, з яким множником її створили. На тому самому механізмі побудовані currying, partial application і composition: часткове застосування аргументів, це просто повернення функції, що замкнула вже передані значення.

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

Обробник події живе довше за функцію, яка його підписала, і продовжує бачити її змінні:

javascript
function setupButton(id) { let clicks = 0; document.getElementById(id).addEventListener('click', () => { clicks++; console.log(`Clicks on button ${id}: ${clicks}`); }); } setupButton('save'); setupButton('cancel');

Кожен обробник пам'ятає власну змінну clicks, навіть після виходу з setupButton. Той самий прийом дає кеш, який переживає виклики:

javascript
function memoize(fn) { const cache = {}; return function(arg) { if (cache[arg]) { console.log('From cache:', arg); return cache[arg]; } const result = fn(arg); cache[arg] = result; console.log('Computed:', arg); return result; }; } const square = memoize(x => x * x); square(4); // Computed: 4 square(4); // From cache: 4

Об'єкт cache існує лише всередині замикання: він недоступний ззовні і не засмічує глобальну область.

Як це працює під капотом

Коли функція створюється, рушій формує для неї лексичне оточення (Lexical Environment), внутрішній об'єкт зі змінними, оголошеними в цій області, і посиланням на зовнішнє оточення. Якщо внутрішня функція використовує змінну ззовні, вона тримає посилання на це оточення, тому збирач сміття не може його прибрати, поки жива сама функція. Саме тому змінні зовнішньої функції доступні й після її завершення.

Спеціально нічого робити не треба: замикання виникає щоразу, коли ви оголошуєте функцію всередині іншої.

javascript
function outer() { const a = 42; return function() { console.log(a); // this is a closure }; }

Де closures використовують на практиці

СценарійПриклад
Приватні даніХоваємо змінні всередині функції, назовні лише методи
Фабрики функційГенератори, конфігуровані обробники
Модулі та інкапсуляціяПоділ логіки без глобальних змінних
Кешування (memoization)Зберігаємо результати обчислень між викликами
Лічильники та станУтримуємо стан між викликами
Колбеки й обробники подійДоступ до зовнішніх даних усередині функції
Функційне програмуванняCurrying, partial application, compose

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

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

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

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

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