Фундамент Ф1 · урок 7 з 12
Область видимості й замикання
Замикання — найчастіше питання про JavaScript на співбесідах. І це не теоретична дивина: на ньому стоять фабрики валідаторів у формах Angular, обробники подій, які «памʼятають» свою задачу, і половина операторів RxJS. Щоб зрозуміти замикання, спершу треба зрозуміти, де змінна «видна», — з цього й почнемо.
Область видимості: де змінну видно
Змінна, оголошена через let чи const, існує лише всередині свого блоку — тобто між
фігурними дужками, де її оголосили. Блок функції, if, циклу — кожен створює свою область.
Уяви кімнати з вікнами, через які видно лише назовні. З внутрішньої кімнати видно коридор і двір, а з двору в кімнату — ні. Так і з кодом: зсередини видно змінні зовнішніх блоків, а ззовні внутрішні — ні.
Запусти й прочитай помилку. Потім перенеси console.log(note) всередину if.
Якщо всередині блоку оголосити змінну з тим самим імʼям, що й назовні, внутрішня затінює зовнішню: у блоці буде видно лише її. Код від цього працює, але читати його важко — даруй змінним різні імена.
Чому не var
У var інші правила: вона бачить не блок, а всю функцію, і «піднімається» на її початок. Через це
класичний баг, який трапляється навіть у досвідчених:
setTimeout(fn, 0) відкладає виклик функції «на потім» — після того, як виконається весь
поточний код (детально — у Ф2). Вгадай, що виведе кожен цикл, і запусти.
З var існує одна змінна i на всю функцію. Коли відкладені виклики нарешті спрацьовують, цикл
уже закінчився, і i дорівнює 3 — усі три функції бачать ту саму трійку. З let кожен крок
циклу отримує свою змінну j, і кожна функція бачить своє значення. Звідси правило з першого уроку:
var — ніколи.
Замикання: функція з рюкзаком
Тепер головне. Функція памʼятає змінні з місця, де її створили, — навіть коли те місце вже відпрацювало й давно завершилось. Уяви, що кожна функція носить рюкзак, куди склала все, що бачила навколо в момент створення. Це й називають замиканням (closure).
Ключове тут: функція бере в рюкзак не значення, а саму змінну. Тому count накопичується
між викликами, а кожен новий виклик makeCounter створює новий рюкзак — і лічильники не заважають один
одному.
Зараз обидва лічильники ділять одну глобальну змінну, і b() продовжує рахунок a.
Перепиши makeCounter так, щоб кожен лічильник рахував окремо: вивід має бути 1, 2,
1.
let count = 0 всередину makeCounter — тоді кожен виклик створить свою змінну,
і повернена стрілка забере саме її.Де замикання трапляються щодня
Фабрики функцій. Функція, яка приймає налаштування й повертає іншу функцію, що їх памʼятає. Саме так зроблено валідатор з параметром у модулі 6:
function maxWords(limit) {
return (value) => { // стрілка памʼятає limit
const words = value.trim().split(/\s+/).length;
return words > limit ? { maxWords: limit } : null;
};
}
const titleCheck = maxWords(12); // limit = 12 поїхав у рюкзак
Обробники подій. Стрілка в обробнику памʼятає задачу, для якої її створили:
for (const task of tasks) {
button.addEventListener('click', () => remove(task.id)); // кожна стрілка — зі своїм task
}
Прихований стан. count у makeCounter ззовні не змінити ніяк — лише через повернену
функцію. Так у JavaScript ховали дані задовго до приватних полів класу.
Напиши фабрику multiplier(factor): вона повертає функцію, яка множить своє число на
factor. Вивід має бути 10 15.
function multiplier(factor) { return (n) => n * factor; }varу циклі з відкладеними функціями — всі бачать останнє значення. Пишиlet.- Очікування, що замикання «сфотографувало» значення. Воно памʼятає змінну: змінилась змінна — функція бачить нове.
- Спроба прочитати змінну з внутрішнього блоку ззовні —
ReferenceError: … is not defined.
Підсумок
- Область видимості — блок у фігурних дужках. Зсередини видно назовні, ззовні всередину — ні.
varбачить усю функцію й ламає цикли з відкладеними викликами.letдає кожному кроку свою змінну.- Замикання — функція памʼятає змінні місця, де її створили, навіть коли те місце вже відпрацювало.
- Фабрики (
maxWords(12)), обробники з даними, прихований стан — усе це замикання.