Замикання (Closures)
JavaScript Closures замикання — прозора скринька що зберігає конверт всередині захищаючи від зовнішнього доступу
Замикання -- одна з найважливіших концепцій JavaScript. Якщо ти розумієш замикання, ти розумієш, як працює JavaScript "під капотом". Добра новина: ти вже використовував замикання, навіть не знаючи про це! Згадай createGreeter з уроку про функції (Block 4, урок 4.6) -- це було замикання.
Лексичне оточення (Lexical Environment)
Коли JavaScript виконує код, кожен блок { } (функція, if, for) створює лексичне оточення -- місце, де зберігаються змінні:
const name = "Олексій"; // глобальне оточення
function greet() {
const greeting = "Привіт"; // оточення функції greet
console.log(`${greeting}, ${name}!`);
}
greet(); // "Привіт, Олексій!"
Як JavaScript знайшов name всередині greet? Через scope chain (ланцюжок областей видимості):
- Шукає
nameв оточенніgreet-- не знайдено - Піднімається до зовнішнього (глобального) оточення -- знайдено!
Ключове слово -- лексичне. Це означає, що область видимості визначається тим, де функція написана в коді, а не тим, де вона викликана.
Що таке замикання?
Замикання -- це функція, яка "запам'ятовує" змінні зі свого зовнішнього оточення, навіть після того як зовнішня функція завершилася.
function createCounter() {
let count = 0; // ця змінна "живе" в замиканні
return function() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
Що тут відбувається:
createCounter()виконалась і повернула внутрішню функцію- Змінна
countмала б зникнути (функція завершилась!) - Але внутрішня функція тримає посилання на оточення, де
countіснує - Тому
countзалишається "живою" -- це і є замикання
Кожен виклик -- нове замикання
const counter1 = createCounter();
const counter2 = createCounter();
console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 1 (окремий count!)
console.log(counter1()); // 3
Кожен виклик createCounter() створює нове лексичне оточення з власним count.
Фабрики функцій
Замикання ідеально підходять для створення "налаштованих" функцій:
function createGreeter(greeting) {
return (name) => `${greeting}, ${name}!`;
}
const hello = createGreeter("Привіт");
const goodbye = createGreeter("До побачення");
console.log(hello("Олексій")); // "Привіт, Олексій!"
console.log(goodbye("Марія")); // "До побачення, Марія!"
function createMultiplier(factor) {
return (number) => number * factor;
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
const toPercent = createMultiplier(100);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(toPercent(0.75)); // 75
Приватні змінні
Замикання -- єдиний спосіб створити по-справжньому приватні дані в JavaScript (до появи # в класах):
function createBankAccount(initialBalance) {
let balance = initialBalance; // приватна змінна
return {
deposit(amount) {
if (amount <= 0) throw new Error("Сума повинна бути додатною");
balance += amount;
return balance;
},
withdraw(amount) {
if (amount > balance) throw new Error("Недостатньо коштів");
balance -= amount;
return balance;
},
getBalance() {
return balance;
},
};
}
const account = createBankAccount(1000);
console.log(account.getBalance()); // 1000
account.deposit(500);
console.log(account.getBalance()); // 1500
account.withdraw(200);
console.log(account.getBalance()); // 1300
// Прямий доступ до balance неможливий:
console.log(account.balance); // undefined
Цей патерн називається Module Pattern і був основним способом організації коду до появи ES6 модулів. Навіть зараз він корисний для інкапсуляції стану.
Замикання в циклах -- класична пастка
Це одне з найпопулярніших питань на співбесідах:
// ПРОБЛЕМА: всі функції "бачать" одну й ту саму змінну i
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
// Виведе: 3, 3, 3 (а не 0, 1, 2!)
Чому? var не створює нову область видимості для кожної ітерації. Коли setTimeout спрацює, цикл вже завершився і i === 3.
Рішення 1: let (сучасний спосіб)
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
// Виведе: 0, 1, 2
let створює нову змінну для кожної ітерації -- кожне замикання отримує своє значення.
Рішення 2: замикання через IIFE (старий спосіб)
for (var i = 0; i < 3; i++) {
((j) => {
setTimeout(() => {
console.log(j);
}, 1000);
})(i);
}
// Виведе: 0, 1, 2
Завжди використовуй let у циклах. var з замиканнями -- джерело багів. Знати про цю проблему корисно для співбесід, але в реальному коді просто пиши let.
IIFE -- Immediately Invoked Function Expression
Функція, яка виконується одразу після оголошення:
(function() {
const secret = "Цю змінну ніхто не побачить";
console.log(secret);
})();
// Або arrow function
(() => {
const secret = "Теж приватна";
console.log(secret);
})();
IIFE використовували для створення приватної області видимості до появи let/const та модулів. Зараз це рідкісний патерн, але ти зустрінеш його в старому коді.
Практичні патерни з замиканнями
Мемоїзація -- кешування результатів
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (key in cache) {
console.log("З кешу!");
return cache[key];
}
const result = fn(...args);
cache[key] = result;
return result;
};
}
const expensiveCalc = memoize((n) => {
console.log("Обчислюю...");
return n * n;
});
console.log(expensiveCalc(5)); // "Обчислюю..." → 25
console.log(expensiveCalc(5)); // "З кешу!" → 25
console.log(expensiveCalc(3)); // "Обчислюю..." → 9
Once -- функція, що виконується лише раз
function once(fn) {
let called = false;
let result;
return function(...args) {
if (!called) {
called = true;
result = fn(...args);
}
return result;
};
}
const initialize = once(() => {
console.log("Ініціалізація!");
return { ready: true };
});
console.log(initialize()); // "Ініціалізація!" → { ready: true }
console.log(initialize()); // { ready: true } (без повторного виклику)
Debounce -- затримка виконання
function debounce(fn, delay) {
let timerId;
return function(...args) {
clearTimeout(timerId);
timerId = setTimeout(() => {
fn(...args);
}, delay);
};
}
// Пошук виконається тільки через 300мс після останнього натискання
const search = debounce((query) => {
console.log(`Шукаю: ${query}`);
}, 300);
search("J");
search("Ja");
search("Jav");
search("Java");
search("JavaScript"); // Виконається тільки цей виклик
debounce -- один з найкорисніших патернів у frontend-розробці. Його використовують для пошуку в реальному часі, обробки scroll/resize подій тощо. Ти зустрінеш його в React (Block 8).
Замикання в обробниках подій
Замикання пояснюють, чому обробники подій "бачать" змінні з зовнішнього scope:
function setupButtons() {
const buttons = document.querySelectorAll(".btn");
buttons.forEach((button, index) => {
button.addEventListener("click", () => {
// Кожен обробник "замкнув" свій index
console.log(`Натиснуто кнопку ${index}`);
});
});
}
Практика: лічильник з API
Створимо лічильник з можливістю збільшення, зменшення та отримання значення:
function createCounter(initial = 0) {
let value = initial;
return {
increment() { return ++value; },
decrement() { return --value; },
getValue() { return value; },
reset() {
value = initial; // initial теж "замкнута"!
return value;
},
};
}
const counter = createCounter(10);
console.log(counter.increment()); // 11
console.log(counter.increment()); // 12
console.log(counter.decrement()); // 11
console.log(counter.getValue()); // 11
console.log(counter.reset()); // 10
Підсумок
- Лексичне оточення -- місце, де зберігаються змінні, визначається положенням у коді
- Замикання -- функція, яка запам'ятовує змінні зовнішнього оточення
- Кожен виклик зовнішньої функції створює нове замикання
- Замикання дозволяють створювати приватні змінні та фабрики функцій
letу циклах вирішує класичну проблему замикання зvar- Практичні патерни: memoize, once, debounce
Що далі?
У наступному уроці -- прототипи та класи: як працює наслідування в JavaScript та як писати об'єктно-орієнтований код з ES6 class.
Корисні посилання:
- javascript.info: Замикання -- детальне пояснення з ілюстраціями (українською)
- MDN: Closures -- документація з прикладами
- You Don't Know JS: Scope & Closures -- глибоке занурення (англійською)