Замикання
Замикання (closure) це функція, яка «пам'ятає» змінні з області, де її було створено, навіть якщо ця область уже завершила виконання. Інакше кажучи, замикання це функція разом із лексичним оточенням, у якому вона народилася.
Теорія
TL;DR
- Замикання виникає, коли функція оголошена всередині іншої функції й використовує її змінні.
- Зовнішня функція може завершитися, але захоплені змінні не знищуються, поки на них є посилання.
- Замикання зберігає посилання на змінну, а не її копію, тому стан живе між викликами.
- Кожен виклик зовнішньої функції створює нове незалежне оточення зі своїми змінними.
- Це основний спосіб робити приватні дані, лічильники, фабрики функцій і мемоізацію.
- Класична пастка:
varу циклі разом ізsetTimeoutдає одну спільну змінну на всі ітерації.
Швидкий приклад
function outer() {
let count = 0; // змінна зовнішньої функції
function inner() {
count++;
console.log(count);
}
return inner;
}
const counter = outer(); // викликаємо зовнішню функцію
counter(); // 1
counter(); // 2
counter(); // 3Що відбувається:
outer()виконується і створює зміннуcountта внутрішню функціюinner.- Повертає
inner, яка пам'ятає, де її було створено. - Навіть після завершення
outer()зміннаcountне знищується, бо на неї є посилання зinner.
Коротко ланцюжок такий:
outer() -> створило count -> inner() -> запам'ятало countЗамикання це функція плюс її оточення
Кожне замикання зберігає лексичне оточення (scope), тобто набір змінних, до яких воно має доступ.
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) дадуть дві різні функції з двома різними оточеннями, які нічого не знають одна про одну.
Приватні змінні та стан
Замикання це штатний спосіб зробити дані недоступними ззовні.
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.
Замикання зберігає посилання, а не значення
Функція пам'ятає не значення, а посилання на змінну в зовнішньому оточенні.
function make() {
let value = 1;
return () => console.log(value++);
}
const f = make();
f(); // 1
f(); // 2, змінна "value" жива між викликамиСаме тому лічильник працює: обидва виклики читають і змінюють ту саму комірку пам'яті.
Класична помилка з циклом
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 1000);
}
// через 1 секунду: 3, 3, 3Чому так:
- Усі функції замкнули одну й ту саму змінну
i, оголошену черезvarу функціональній області видимості. - Коли спрацьовує таймер, цикл уже завершився і
iдорівнює3.
Виправлення через let, який створює нову змінну на кожній ітерації:
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із зовнішнього оточення.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.