Skip to main content

Цикл з var і setTimeout

Цикл із 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.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.