Користь від closures
Closure (замикання), це функція, яка «пам'ятає» свою лексичну (зовнішню) область навіть тоді, коли виконується вже поза нею. Інакше кажучи, функція має доступ до змінних з того місця, де її створили, а не з того, звідки її викликали, і саме на цьому тримаються приватні дані, фабрики функцій, кеші та більшість патернів функційного програмування.
Теорія
TL;DR
- Closure, це пара «функція + лексичне оточення, у якому її створено».
- Внутрішня функція тримає посилання на оточення зовнішньої, тому змінні не зникають після її завершення.
- Головні застосування: приватні дані, фабрики функцій, стан між викликами, мемоізація, currying.
- Замикання створюється автоматично, робити для цього нічого спеціального не треба.
- Closure зберігає посилання на змінну, а не її копію на момент створення.
- Зайві закриті посилання тримають пам'ять, тож великі об'єкти в закритому оточенні, це реальний ризик витоку.
Швидкий приклад
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, «збережена пам'ять» функції.
Приватні дані та інкапсуляція
Змінні зовнішньої функції недосяжні ззовні, але доступні всім функціям, що в ній створені. Це дає приватний стан без класів і без #-полів:
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 дозволяє «вшити» конфігурацію у функцію на момент створення:
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: часткове застосування аргументів, це просто повернення функції, що замкнула вже передані значення.
Збереження стану: обробники подій і мемоізація
Обробник події живе довше за функцію, яка його підписала, і продовжує бачити її змінні:
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. Той самий прийом дає кеш, який переживає виклики:
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), внутрішній об'єкт зі змінними, оголошеними в цій області, і посиланням на зовнішнє оточення. Якщо внутрішня функція використовує змінну ззовні, вона тримає посилання на це оточення, тому збирач сміття не може його прибрати, поки жива сама функція. Саме тому змінні зовнішньої функції доступні й після її завершення.
Спеціально нічого робити не треба: замикання виникає щоразу, коли ви оголошуєте функцію всередині іншої.
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визначається способом виклику і закриттям не фіксується (крім стрілкових функцій, які беруть його лексично).
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.