Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що не так з var у циклі та setTimeout?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**var у циклі з setTimeout** виводить фінальне значення для всіх колбеків, бо `var` має функціональну область видимості і всі ітерації ділять одну змінну. ```javascript for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); // 0, 1, 2 } ``` **Ключове:** `let` створює нову прив'язку на кожну ітерацію. `var` дає одну прив'язку для всіх.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**var у циклі з setTimeout** - всі колбеки виводять фінальне значення лічильника, а не значення своєї ітерації, бо `var` має функціональну область видимості і всі ітерації діляться однією змінною. ## Теорія ### TL;DR - `var i` - одна змінна на всю функцію. Всі три колбеки `setTimeout` посилаються на неї. - `setTimeout` - макрозадача. Вона виконується після завершення циклу, коли `i` вже дорівнює `3`. - `let i` створює нову прив'язку для кожної ітерації. Кожен колбек бачить своє значення. - Фікс: `let` замість `var`, або IIFE у старому ES5-коді. ### Швидкий приклад ```javascript // Проблема: var (виводить 3, 3, 3) for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); // всі колбеки закриваються над однією i } // Фікс: let (виводить 0, 1, 2) for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); // кожна ітерація має власну i } ``` `var i` піднімається (hoisting) у функціональну область. До моменту виконання колбеків цикл вже завершився, і `i` дорівнює `3`. З `let i` рушій створює нову прив'язку для кожної ітерації, тому кожен колбек захоплює окреме значення. ### Чому var тут не працює Цикл виконується синхронно. JavaScript додає кожен колбек `setTimeout` до черги макрозадач, яка обробляється лише після завершення поточного синхронного коду. Всі три колбеки запускаються, коли `i` вже `3`. Вони читають одну і ту саму змінну і виводять однакове значення. Варіант із `setTimeout(..., 0)` ловить людей на співбесідах частіше, ніж із `100ms`. Нуль мілісекунд підсвідомо відчувається як «зараз». Але `0ms` не означає синхронно - колбек все одно чекає в черзі. `let` вирішує це, бо специфікація визначає нову прив'язку `i` для кожної ітерації. Кожне замикання (closure) захоплює окрему прив'язку, а не спільну змінну. Це стає зрозумілішим, якщо знати [як працює Event Loop](/questions/event-loop) і [що таке замикання (closure)](/questions/closure). ### Що використовувати - Цикл із `setTimeout`, Promises або обробниками подій: завжди `let`. - Кодова база ES5 без `let`: IIFE з передачею значення як аргументу: `(function(i){ setTimeout(() => console.log(i), 100); })(i)`. - `forEach` або `.map()`: безпечні за замовчуванням, кожен аргумент колбека отримує власну область видимості. ### Як це обробляє рушій V8 піднімає `var i` на початок функції, створюючи один запис Lexical Environment для всього циклу. `let i` вмикає механізм «for loop block scope» зі специфікації: рушій створює новий Lexical Environment на кожну ітерацію і копіює туди поточне значення прив'язки. Саме тому варто розуміти [hoisting (підняття)](/questions/hoisting) перед тим, як це питання з'явиться на співбесіді. ### Типові помилки **Помилка 1: думати, що `setTimeout(..., 0)` виконується під час циклу** ```javascript for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); // все одно 3, 3, 3 } ``` Затримка `0ms` не означає синхронне виконання. Колбек все одно потрапляє в чергу макрозадач і виконується після завершення циклу. **Помилка 2: IIFE без передачі `i` як аргументу** ```javascript // Неправильно: все одно закривається над зовнішнім i for (var i = 0; i < 3; i++) { (function() { setTimeout(() => console.log(i), 100); // 3, 3, 3 })(); } // Правильно: i стає локальним параметром for (var i = 0; i < 3; i++) { (function(i) { setTimeout(() => console.log(i), 100); // 0, 1, 2 })(i); } ``` IIFE створює нову область видимості, але якщо не передати `i` як аргумент, внутрішня функція все одно читає зовнішній `i`. Передача у вигляді аргументу створює локальну копію на момент виклику. **Помилка 3: спроба використати `const` у `for`-циклі** ```javascript for (const i = 0; i < 3; i++) {} // SyntaxError: Assignment to constant variable ``` `const` забороняє повторне присвоєння, тому крок `i++` одразу ламає виконання. Тут потрібен `let`. ### Питання на співбесіді **Q:** Чому синхронний `console.log(i)` усередині циклу виводить `0, 1, 2`, а `setTimeout` - ні? **A:** Синхронний код виконується прямо у тілі циклу, де `i` має значення поточної ітерації. `setTimeout` відкладає виконання через Event Loop на час після завершення циклу, коли `i` вже `3`. **Q:** Що з `for...of` і `var`? **A:** Та сама проблема. `for (var x of arr)` перезаписує `x` на кожній ітерації. Всі колбеки закриваються над одним `x` і читають його фінальне значення. `for (let x of arr)` виправляє це. **Q:** Два способи виправити це в ES5? **A:** Перший: IIFE з передачею аргументу: `(function(i){ setTimeout(() => console.log(i), 100); })(i)`. Другий: `.forEach()`, який передає кожен елемент як аргумент функції і автоматично створює нове замикання на кожен виклик. **Q:** Чи може `forEach` мати ту ж проблему? **A:** Так, якщо закриватися над зовнішнім `var` замість аргументу колбека. `arr.forEach(function(){ setTimeout(() => console.log(x), 100); })` прочитає `x` у момент виконання, а не реєстрації. Аргумент колбека завжди безпечний. ## Приклади ### Базовий: класична проблема ```javascript for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // Вивід: 3, 3, 3 for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // Вивід: 0, 1, 2 ``` Три колбеки, одна спільна змінна, черга яка спрацьовує після циклу. Ось уся проблема. `let` створює три окремі прив'язки замість однієї. ### Проміжний: `for...of` має ту саму пастку ```javascript // var ламає і for...of for (var item of ['a', 'b', 'c']) { setTimeout(() => console.log(item), 0); } // Вивід: 'c', 'c', 'c' // let виправляє for (let item of ['a', 'b', 'c']) { setTimeout(() => console.log(item), 0); } // Вивід: 'a', 'b', 'c' ``` Те саме правило видимості діє в `for...of`. Оголошення `var item` піднімається з циклу, тому всі колбеки бачать останнє присвоєне значення. `let item` створює нову прив'язку для кожної ітерації.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.