Skip to main content

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

Closure (замикання) - функція, яка зберігає доступ до змінних зі свого місця народження, навіть після того як те місце завершило виконання.

Теорія

TL;DR

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

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

javascript
function createCounter() { let count = 0; // кладеться в "рюкзак" return function() { count++; console.log(count); }; } const counter = createCounter(); counter(); // 1 counter(); // 2 - count зберігається між викликами

createCounter вже повернулася, але count нікуди не зник. Повернена функція захопила його в момент створення.

Ключова різниця

Глобальні змінні вирішують ту саму задачу "запам'ятати щось", але засмічують весь простір імен. Параметри скидаються при кожному виклику. Замикання дають постійний приватний стан, прив'язаний до конкретного екземпляра функції. Виклич createCounter() двічі - отримаєш два незалежних лічильники, кожен зі своїм count.

Коли використовувати

  • Приватні лічильники і стан: createCounter() повертає функцію з прихованим постійним станом.
  • Обробники подій: makeHandler(id) захоплює id, щоб колбек завжди знав який елемент його викликав.
  • Модульний патерн: createLogger(level) фільтрує логи за захопленим рівнем, без класу.
  • React хуки: useEffect і useCallback використовують замикання для захоплення стану і пропсів між рендерами.

Уникай замикань коли функція не має стану. Просто передавай аргументи.

Як це працює всередині

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

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

Var у циклах - класична пастка:

javascript
for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); // виведе 3, 3, 3 } // Виправлення через let - кожна ітерація отримує власний binding: for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); // виведе 0, 1, 2 }

З var всі три колбеки діляться одним i. Коли вони нарешті запускаються, цикл вже завершений і i дорівнює 3. Такий самий патерн ламав jQuery-обробники кнопок у старих кодових базах.

Думати що зовнішні змінні зникають після return:

javascript
function makeAdder(x) { return y => x + y; // x захоплено, воно не зникло } const add5 = makeAdder(5); console.log(add5(3)); // 8 - x все ще живе

x живе стільки, скільки живе add5.

Витік пам'яті через DOM-слухачі:

javascript
function attach(el) { const bigData = new Array(1000000).fill('x'); // захоплено замиканням el.addEventListener('click', () => console.log(bigData.length)); // bigData не може бути звільнено поки el у DOM }

Замикання тримає посилання на bigData, блокуючи збирач сміття. Використовуй лише те що потрібно всередині обробника, або явно видаляй слухача.

Де зустрічається

  • React: useCallback і useEffect захоплюють стан і пропси для стабільних актуальних обробників.
  • Lodash: _.debounce(fn, ms) повертає замикання що відстежує час останнього виклику.
  • Express: фабрики middleware типу auth(userId) захоплюють userId для обробника запиту.
  • Node.js: EventEmitter.on('event', handler) де handler закриває над специфічним для слухача станом.

Питання на співбесіді

Q: Що відбувається з пам'яттю коли замикання тримає великий об'єкт?
A: Об'єкт залишається в пам'яті поки замикання досяжне. Якщо воно прив'язане до DOM-елемента що ніколи не видаляється, маємо витік. Chrome DevTools heap snapshots показують які замикання утримують об'єкти від збору.

Q: Яка різниця між областю видимості замикання і блочною областю?
A: Блочна область (let, const) створює новий binding на блок або ітерацію циклу. Область замикання охоплює весь час життя функції і зберігається між кількома викликами внутрішньої функції.

Q: Як реалізувати приватні поля без class fields з ES2022?
A: Замикання в конструкторі: class Counter { constructor() { let count = 0; this.inc = () => ++count; } }. Кожен екземпляр отримує власний count через своє замикання.

Q: Чому useEffect у React іноді читає застарілий стан?
A: Ефект захоплює змінні на момент свого запуску. Якщо стан змінюється пізніше, замикання тримає старе значення. Додай змінну в масив залежностей щоб отримати нове замикання, або використовуй useRef для значень що потрібно читати без перезапуску ефекту.

Приклади

Базовий: приватний стан через лічильник

javascript
function createCounter() { let count = 0; return { increment() { count++; }, decrement() { count--; }, value() { return count; } }; } const c = createCounter(); c.increment(); c.increment(); c.decrement(); console.log(c.value()); // 1 // count недоступний ззовні: console.log(c.count); // undefined

Три методи діляться одним count. Ніщо за межами createCounter не може прочитати або змінити його напряму. Це модульний патерн що існував до стандартизації ES-модулів.

Середній: обробник подій із захопленим контекстом

javascript
function setupButtons(labels) { labels.forEach((label, index) => { const button = document.createElement('button'); button.textContent = label; // Кожен колбек закриває над своїми label і index button.addEventListener('click', () => { console.log(`Натиснуто: ${label} на позиції ${index}`); }); document.body.appendChild(button); }); } setupButtons(['Головна', 'Про нас', 'Контакти']); // Клік по "Про нас" виведе: Натиснуто: Про нас на позиції 1

Оскільки forEach надає кожному колбеку власні прив'язки label і index, кожен обробник точно знає до якої кнопки він належить. Саме цей сценарій ламався з var у класичному for-циклі до появи let.

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

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

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

Дочитали статтю?