Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Цикл з var і setTimeout». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Через дві речі одночасно: `var` має функціональну область видимості, тому на весь цикл створюється одна змінна `i`, а `setTimeout` виконується пізніше, коли цикл уже завершився.** Усі колбеки замикаються на ту саму змінну, потрапляють у чергу макрозадач і запускаються лише після того, як поточний стек JavaScript спорожніє, а на той момент `i` вже має фінальне значення `3`. Виправлення: замінити `var` на `let`, який створює нове зв'язування на кожну ітерацію, або ізолювати поточне значення через IIFE, третій аргумент `setTimeout` чи `bind`. ```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 } ``` **Ключове:** `var` дає одну спільну змінну на весь цикл, а колбек читає її вже після завершення циклу.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Цикл із `var` і `setTimeout` друкує три однакові числа через дві речі одночасно: `var` створює одну змінну на весь цикл, а колбеки таймера виконуються вже після того, як цикл завершився.** Обидві причини мають збігтися: окремо ні асинхронність, ні `var` такої поведінки не дають. ## Теорія ### TL;DR - `var` має **функціональну** область видимості: у циклі створюється **одна** змінна `i` на всю функцію, а не нова на кожну ітерацію. - Усі колбеки `setTimeout` замикаються на **одну й ту саму** змінну. - `setTimeout` виконується **пізніше**: колбеки потрапляють у чергу макрозадач. - Черга обробляється тільки після того, як поточний стек JavaScript спорожніє. - До моменту першого спрацювання таймера цикл уже завершено, і `i` має **фінальне значення** (наприклад, `3`). - Виправляється через `let`, IIFE, третій аргумент `setTimeout` або `bind`. ### Швидкий приклад Симптом: ```javascript for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); } // 3 // 3 // 3 ``` ### Причина 1: `var`, це функціональна область видимості У циклі `for (var i = 0; i < 3; i++) { ... }` створюється **одна** змінна `i` на всю функцію (або на глобальну область), а не нова на кожну ітерацію. Усі три стрілкові функції, передані в `setTimeout`, замикаються не на «значення `i` в момент створення», а на **саме зв'язування** (binding) єдиної змінної. Тому після завершення циклу всі три колбеки читають одну й ту саму комірку пам'яті, у якій лежить `3`. ### Причина 2: `setTimeout` виконується пізніше Колбеки `setTimeout` не виконуються одразу, навіть із затримкою `0`. Вони потрапляють у **чергу макрозадач** і запускаються тільки **після того, як поточний стек JavaScript спорожніє**, тобто після того, як синхронний код (включно з усім циклом) відпрацював. Порядок такий: 1. Цикл виконується повністю, три рази викликаючи `setTimeout` і плануючи три колбеки. 2. Цикл завершується, `i` стає рівним `3`. 3. Стек порожніє, event loop дістає колбеки з черги й виконує їх один за одним. 4. Кожен колбек читає поточне значення `i`, а це вже `3`. ### Як виправити **Варіант 1: `let` (блокова область видимості)** ```javascript for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); // 0, 1, 2 } ``` Специфікація вимагає, щоб `for` з `let` створював **нове зв'язування на кожну ітерацію** і копіював у нього попереднє значення. Кожен колбек замикається на власну копію. **Варіант 2: IIFE або замикання з передачею поточного `i`** ```javascript for (var i = 0; i < 3; i++) { (function (j) { setTimeout(() => console.log(j), 0); // 0, 1, 2 })(i); } ``` Виклик функції створює нову область видимості, а параметр `j` отримує **копію** значення `i` на момент виклику. **Варіант 3: передати аргументом у `setTimeout`** ```javascript for (var i = 0; i < 3; i++) { setTimeout(j => console.log(j), 0, i); // 0, 1, 2 } ``` Третій і наступні аргументи `setTimeout` передаються в колбек, і їхні значення фіксуються в момент планування таймера. **Варіант 4: `bind`** ```javascript for (var i = 0; i < 3; i++) { setTimeout(console.log.bind(null, i), 0); // 0, 1, 2 } ``` `bind` створює нову функцію з наперед зафіксованим аргументом, тобто теж копіює значення одразу. ### Підсумок | Варіант | Як фіксує значення | | --- | --- | | `let` | Нове зв'язування на кожну ітерацію циклу | | IIFE | Нова область видимості на кожен виклик функції | | Аргумент `setTimeout` | Значення передається в колбек у момент планування | | `bind` | Аргумент «вшивається» у нову функцію одразу | > Коротко: `var` дає одну «спільну» змінну на весь цикл, а `setTimeout` запускається, коли цикл уже закінчено. Використовуйте `let` або ізолюйте значення через замикання чи аргументи. ### Типові помилки - Думати, що проблема в затримці, і ставити `setTimeout(..., 100)`. Затримка тут ні до чого: колбек усе одно виконається після циклу. - Вважати, що замикання копіює значення змінної. Замикання зберігає посилання на зв'язування, а не знімок значення. - Замінити `var` на `let` лише в оголошенні всередині тіла циклу, а не в самому `for`. Нове зв'язування на ітерацію дає саме `let` у заголовку циклу. - Використати `var` із `forEach` і вирішити, що проблема зникла «сама». Вона зникла тому, що `forEach` викликає функцію на кожен елемент, а це вже окрема область видимості. - Плутати цей ефект із hoisting. Hoisting пояснює, чому `i` видно до оголошення, а не чому всі колбеки бачать `3`.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.