Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Помилка з var у циклі та closures». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**`var` має функційну, а не блокову область видимості, тому на весь цикл існує одна змінна `i`, і всі колбеки замикають саме її. Коли асинхронні колбеки нарешті виконуються, цикл уже завершився і `i` дорівнює кінцевому значенню, звідси `3, 3, 3` замість `0, 1, 2`. Найпростіше виправлення, це `let`: цикл `for (let i = ...)` створює нове лексичне зв'язування на кожній ітерації. Якщо `var` змінити не можна, значення фіксують через IIFE, фабрику функцій, методи масиву (`forEach`) або третій аргумент `setTimeout`.** ```javascript for (var i = 0; i < 3; i++) setTimeout(() => console.log(i), 0); // 3, 3, 3 for (let i = 0; i < 3; i++) setTimeout(() => console.log(i), 0); // 0, 1, 2 ``` **Ключове:** замикання захоплює змінну, а не її миттєве значення, тож потрібна окрема змінна на ітерацію.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Класика: у циклі з `var` усі колбеки бачать одну й ту саму змінну `i`, бо `var` має функційну область видимості.** Коли колбеки нарешті виконуються, цикл уже завершено, і `i` дорівнює кінцевому значенню, тому замість очікуваних `0, 1, 2` у консоль тричі потрапляє `3`. ## Теорія ### TL;DR - `var` має функційну область видимості: на весь цикл існує рівно одна змінна. - Замикання захоплює змінну, а не її значення на момент створення функції. - Асинхронний колбек читає цю змінну вже після завершення циклу, коли в ній кінцеве значення. - `let` у заголовку `for` створює нове зв'язування на кожній ітерації, і проблема зникає. - Якщо `var` лишається, значення фіксують через IIFE, фабрику, `forEach` або аргумент `setTimeout`. ### Швидкий приклад ```javascript for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); } // 3, 3, 3 ``` ### Чому так відбувається Оголошення `var i` піднімається (hoisting) до початку функції або до глобальної області, тож змінна одна на всі три ітерації. Кожен колбек, переданий у `setTimeout`, замикає посилання на цю єдину змінну, а не копіює її значення. Далі вмикається event loop: навіть із затримкою `0` колбек потрапляє в чергу макрозадач і виконається лише після того, як синхронний код (тобто весь цикл) завершиться. На той момент умова `i < 3` уже хибна, бо `i === 3`. Усі три колбеки читають ту саму комірку і виводять `3`. ### Виправлення 1: `let` або `const` У ES6 заголовок `for (let i = ...)` отримує окреме лексичне зв'язування на кожну ітерацію: рушій копіює поточне значення в нову змінну перед наступним кроком. Кожен колбек замикає власну змінну: ```javascript for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); // 0, 1, 2 } ``` Це найпростіший і найбезпечніший шлях, і саме тому в сучасному коді `var` у циклах не використовують. Для `for...of` з незмінним елементом підійде і `const`. ### Виправлення 2: зафіксувати значення вручну Якщо з якоїсь причини потрібен саме `var`, значення поточної ітерації треба перекласти в окрему змінну. **IIFE (функція, що викликає себе одразу)** створює нову область видимості і «заморожує» поточне значення `i`: ```javascript for (var i = 0; i < 3; i++) { (function(iCopy) { setTimeout(() => console.log(iCopy), 0); })(i); } ``` **Фабрика функцій** робить те саме, але читабельніше: параметр `x` живе у власному оточенні кожної створеної функції: ```javascript function makeLogger(x) { return () => console.log(x); } for (var i = 0; i < 3; i++) { setTimeout(makeLogger(i), 0); } ``` **Методи масиву** (`forEach`, `map`) знімають питання взагалі: параметр колбека вже є окремою змінною на кожен елемент: ```javascript [0, 1, 2].forEach(i => setTimeout(() => console.log(i), 0)); ``` **Третій аргумент `setTimeout`** передає значення в колбек напряму (підтримується в браузерах і в Node.js): ```javascript for (var i = 0; i < 3; i++) { setTimeout(x => console.log(x), 0, i); } ``` ### Короткий чек-лист - За замовчуванням використовуйте `let` або `const` у циклах, це найпростіший і найбезпечніший шлях. - Якщо `var` неминучий, зафіксуйте значення через IIFE, фабрику або передавання аргумента. - Пам'ятайте: замикання захоплюють змінну, а не її миттєве значення, а у `var` вона одна на весь цикл. ### Типові помилки - **Вважати, що проблема в `setTimeout`.** Таймер ні до чого: те саме буде з будь-яким відкладеним викликом, обробником події чи промісом. - **Думати, що затримка `0` означає «негайно».** Колбек усе одно піде в чергу і виконається після синхронного коду. - **Замінювати `var` на `let` лише в тілі циклу.** Нове зв'язування на ітерацію дає саме `let` у заголовку `for`, а не окрема змінна десь усередині блоку (хоча копія всередині блоку теж працює). - **Використовувати `var` у `for...in` і `for...of` з колбеками.** Пастка та сама, хоча значення виглядають «свіжими». - **Плутати з `this`.** Тут проблема суто в області видимості змінної; контекст виклику на це не впливає.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.