Пошук уроків, статей та іншого контенту
Дізнайтеся, як функція зберігає доступ до свого зовнішнього лексичного середовища навіть після завершення зовнішньої функції. Побудуйте правильну ментальну модель замикання без поширеного спрощення про «копіювання змінних».
Замикання (closure) — це функція разом із посиланням на лексичне середовище, у якому її було створено.
Завдяки замиканню функція може звертатися до змінних зовнішньої функції навіть після того, як виконання зовнішньої функції завершилося.
function createGreeting(name) {
function greet() {
return `Привіт, ${name}!`;
}
return greet;
}
const greetOlena = createGreeting("Олено");
console.log(greetOlena()); // Привіт, Олено!Під час виклику createGreeting("Олено") створюється змінна name і функція greet.
Після завершення createGreeting її виклик припиняється, але greet усе ще має доступ до name. Функція greet разом із цим доступом і є замиканням.
Щоб зрозуміти замикання, потрібно розділяти:
код функції;
лексичне оточення, у якому функція була створена;
поточне лексичне оточення, у якому функція виконується.
Лексичне оточення містить прив’язки імен до значень:
const language = "JavaScript";
function showLanguage() {
console.log(language);
}
showLanguage(); // JavaScriptУ функції showLanguage немає локальної змінної language, тому JavaScript шукає її у зовнішньому лексичному оточенні.
Пошук відбувається ланцюжком:
локальне оточення функції;
оточення зовнішньої функції;
глобальне оточення;
якщо змінну не знайдено — помилка ReferenceError.
Цей ланцюжок називають ланцюжком лексичних оточень.
Замикання виникає, коли внутрішня функція використовує змінну із зовнішнього оточення і стає доступною за межами цього оточення.
function makeCounter() {
let count = 0;
return function increment() {
count += 1;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3Послідовність подій:
Викликається makeCounter.
Створюється змінна count зі значенням 0.
Створюється функція increment.
increment використовує count, тому зберігає доступ до зовнішнього оточення.
makeCounter повертає increment.
Виклик makeCounter завершується.
Виклик counter() продовжує працювати зі змінною count.
Важливо: count не копіюється в counter. Функція має доступ до тієї самої прив’язки змінної.
Поширене спрощення звучить так:
«Функція запам’ятовує копію значень зовнішніх змінних».
Це не зовсім правильна модель.
Функція зберігає доступ до лексичного оточення, а не просто набір скопійованих значень. Тому зміни змінної можуть бути видимими під час наступних викликів.
function createState() {
let value = 10;
return {
getValue() {
return value;
},
increase() {
value += 1;
},
setValue(newValue) {
value = newValue;
}
};
}
const state = createState();
console.log(state.getValue()); // 10
state.increase();
console.log(state.getValue()); // 11
state.setValue(100);
console.log(state.getValue()); // 100Методи getValue, increase і setValue використовують одну й ту саму змінну value.
Зовнішній код не має прямого доступу до value:
console.log(state.value); // undefinedВін може взаємодіяти зі станом лише через методи, які ми повернули.
Кожен виклик зовнішньої функції створює нове лексичне оточення.
function createCounter() {
let count = 0;
return () => {
count += 1;
return count;
};
}
const firstCounter = createCounter();
const secondCounter = createCounter();
console.log(firstCounter()); // 1
console.log(firstCounter()); // 2
console.log(secondCounter()); // 1
console.log(secondCounter()); // 2firstCounter і secondCounter не ділять змінну count.
У кожного виклику createCounter() є власне оточення:
перше містить свій count;
друге містить інший count.
Тому зміна стану одного лічильника не впливає на інший.
Замикання особливо важливі під час створення функцій у циклах.
let створює окрему прив’язкуУ циклі з let кожна ітерація має окрему змінну:
const actions = [];
for (let index = 0; index < 3; index += 1) {
actions.push(() => index);
}
console.log(actions[0]()); // 0
console.log(actions[1]()); // 1
console.log(actions[2]()); // 2Кожна функція зберігає доступ до прив’язки index, створеної для відповідної ітерації.
var використовує одну прив’язкуvar має функціональну, а не блочну область видимості:
const actions = [];
for (var index = 0; index < 3; index += 1) {
actions.push(() => index);
}
console.log(actions[0]()); // 3
console.log(actions[1]()); // 3
console.log(actions[2]()); // 3Усі функції замкнули одну й ту саму змінну index. Після завершення циклу її значення дорівнює 3.
У сучасному JavaScript для таких випадків зазвичай використовують let.
Замикання зберігає доступ до прив’язки, а якщо ця прив’язка містить об’єкт — до самого об’єкта.
function createUser() {
const user = {
name: "Ірина",
role: "developer"
};
return {
getName() {
return user.name;
},
changeName(name) {
user.name = name;
}
};
}
const user = createUser();
console.log(user.getName()); // Ірина
user.changeName("Марія");
console.log(user.getName()); // МаріяТут методи працюють з одним об’єктом user.
Це пов’язано з двома різними поняттями:
змінна user містить посилання на об’єкт;
властивість user.name можна змінити через це посилання.
Замикання часто використовують для створення приватного стану без прямого доступу до змінних.
function createBankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit(amount) {
if (amount <= 0) {
throw new Error("Сума поповнення має бути більшою за нуль");
}
balance += amount;
},
withdraw(amount) {
if (amount <= 0) {
throw new Error("Сума списання має бути більшою за нуль");
}
if (amount > balance) {
throw new Error("Недостатньо коштів");
}
balance -= amount;
},
getBalance() {
return balance;
}
};
}
const account = createBankAccount(1000);
account.deposit(500);
account.withdraw(200);
console.log(account.getBalance()); // 1300
console.log(account.balance); // undefinedЗовнішній код не може змінити balance напряму:
account.balance = 0;
console.log(account.getBalance()); // 1300Присвоєння створило нову властивість об’єкта account, але не змінило замкнену змінну balance.
Це не абсолютний механізм безпеки, але він добре допомагає контролювати спосіб зміни стану.
Параметри функції також є змінними лексичного оточення.
function createMultiplier(multiplier) {
return function multiply(value) {
return value * multiplier;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15double зберігає доступ до параметра multiplier зі значенням 2, а triple — до іншого оточення зі значенням 3.
Так можна створювати спеціалізовані функції на основі загальної логіки.
Замикання часто використовуються в обробниках подій, таймерах і промісах.
function scheduleMessage(message, delay) {
setTimeout(() => {
console.log(message);
}, delay);
}
scheduleMessage("Повідомлення виконано", 1000);Функція, передана в setTimeout, буде викликана пізніше. Зовнішня функція scheduleMessage на той момент уже завершиться, але callback збереже доступ до параметра message.
Інший приклад:
function createLogger(prefix) {
return function log(message) {
console.log(`[${prefix}] ${message}`);
};
}
const errorLog = createLogger("ERROR");
const infoLog = createLogger("INFO");
errorLog("Не вдалося завантажити дані");
infoLog("Завантаження завершено");Результат:
[ERROR] Не вдалося завантажити дані
[INFO] Завантаження завершеноthisЗамикання та this — різні механізми.
Замикання визначає, як функція знаходить змінні за іменем. Значення this визначається способом виклику функції, за винятком стрілкових функцій, які захоплюють this із зовнішнього контексту.
const user = {
name: "Олена",
regularMethod() {
setTimeout(function () {
console.log(this.name);
}, 0);
},
arrowMethod() {
setTimeout(() => {
console.log(this.name);
}, 0);
}
};
user.regularMethod(); // undefined у типовому середовищі
user.arrowMethod(); // ОленаСтрілкова функція не створює власного this, тому використовує this методу arrowMethod.
Це не означає, що стрілкова функція «замкнула this так само, як змінну». Для практичної роботи важливо пам’ятати, що лексичне захоплення this — окрема особливість стрілкових функцій.
Лексичне оточення зберігається, поки воно може бути потрібним функціям або іншим об’єктам.
function createTemporaryData() {
const largeData = new Array(1_000_000).fill("data");
return () => largeData.length;
}
const getDataLength = createTemporaryData();
console.log(getDataLength()); // 1000000largeData продовжує бути доступним, бо його використовує функція getDataLength.
Якщо всі посилання на цю функцію зникнуть, середовище більше не буде доступним для програми й у типовому випадку зможе бути очищене збирачем сміття.
Тому замикання не є витоком пам’яті саме по собі. Проблеми виникають, коли довгоживучий об’єкт утримує замикання, яке своєю чергою утримує непотрібні великі дані або ресурси.
function createReader() {
let value = 1;
const read = () => value;
value = 2;
return read;
}
const read = createReader();
console.log(read()); // 2, а не 1Функція звертається до змінної, а не до копії її початкового значення.
Якщо потрібне саме початкове значення, його можна зберегти окремо:
function createReader() {
let value = 1;
const initialValue = value;
value = 2;
return () => initialValue;
}
const read = createReader();
console.log(read()); // 1let count = 0;
function createCounter() {
return () => {
count += 1;
return count;
};
}
const first = createCounter();
const second = createCounter();
console.log(first()); // 1
console.log(second()); // 2Обидві функції використовують глобальну змінну count.
Якщо лічильники мають бути незалежними, змінну потрібно розмістити всередині createCounter.
var у цикліУсі callback-функції можуть отримати фінальне значення змінної. Для сучасного коду використовуйте let або створюйте окреме оточення на кожній ітерації.
Якщо замкнена змінна містить об’єкт, функція працює з тим самим об’єктом:
function createReader() {
const data = { value: 1 };
return {
read: () => data.value,
data
};
}
const reader = createReader();
reader.data.value = 2;
console.log(reader.read()); // 2Замикання не створює автоматичну копію об’єкта. Для незалежної копії її потрібно створити явно.
Не варто без потреби повертати callback, який утримує великі масиви, DOM-вузли або інші ресурси протягом усього життя програми.
У браузерному коді також важливо вчасно знімати обробники подій, якщо вони більше не потрібні.
Під час розбору коду поставте собі такі запитання:
Де функцію було створено?
Які зовнішні змінні вона використовує?
Яке лексичне оточення містить ці змінні?
Скільки разів було викликано зовнішню функцію?
Чи створило це окремі оточення?
Чи змінюються замкнені змінні?
Чи працюють кілька функцій з однією прив’язкою?
Чи містить замкнена змінна об’єкт, який змінюється?
Наприклад:
function createPair() {
let value = 0;
return {
increment: () => {
value += 1;
},
read: () => value
};
}
const pair = createPair();
pair.increment();
pair.increment();
console.log(pair.read()); // 2increment і read створені в одному виклику createPair, тому вони посилаються на одну прив’язку value.
Замикання — це функція разом із доступом до лексичного оточення, у якому її створили.
Внутрішня функція може працювати із зовнішніми змінними після завершення зовнішньої функції.
Замикання не копіює змінні, а зберігає доступ до їхніх прив’язок.
Кожен виклик зовнішньої функції зазвичай створює окреме оточення.
Кілька внутрішніх функцій можуть спільно використовувати один замкнений стан.
Замикання застосовують для приватного стану, фабрик функцій, callback-функцій, обробників подій і асинхронного коду.
let і const мають блочну область видимості, а var часто спричиняє неочікувану поведінку в циклах.
Замикання та this — різні механізми, які потрібно аналізувати окремо.