← Назад к вопросам
Асинхронная итерация по массиву с задержкой
2.3 Middle🔥 141 комментариев
#Node.js и JavaScript#Алгоритмы и структуры данных
Условие
Напишите функцию, которая проходит по массиву чисел и выводит индекс каждого элемента с задержкой 1 секунда между выводами.
const arr = [10, 20, 30, 40, 50];
// Ваша функция должна вывести:
// 0 (через 1 сек)
// 1 (через 2 сек)
// 2 (через 3 сек)
// и т.д.
Подвох
Популярная ошибка - использование var в цикле for с setTimeout, что приводит к выводу последнего индекса для всех итераций из-за особенностей замыканий.
Что проверяется
- Понимание замыканий
- Разница между var/let в циклах
- Работа с асинхронным кодом
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение
Вариант 1: Использование let (Рекомендуется)
const arr = [10, 20, 30, 40, 50];
for (let i = 0; i < arr.length; i++) {
setTimeout(() => {
console.log(i);
}, i * 1000);
}
// Вывод:
// 0 (через 1 сек)
// 1 (через 2 сек)
// 2 (через 3 сек)
// 3 (через 4 сек)
// 4 (через 5 сек)
Почему это работает:
letсоздаёт новое замыкание для каждой итерации- Каждый setTimeout получает свою копию переменной i
- Поэтому каждый вывод показывает правильное значение
Вариант 2: Использование IIFE (Старый подход с var)
const arr = [10, 20, 30, 40, 50];
for (var i = 0; i < arr.length; i++) {
(function(index) {
setTimeout(() => {
console.log(index);
}, index * 1000);
})(i);
}
Как это работает:
- Сразу вызываем IIFE (Immediately Invoked Function Expression)
- Передаём текущее значение
iкак аргументindex - Каждая IIFE создаёт своё замыкание с правильным значением
- setTimeout получает правильное
indexиз замыкания
Вариант 3: Использование async/await (Современный подход)
const arr = [10, 20, 30, 40, 50];
async function iterateWithDelay() {
for (let i = 0; i < arr.length; i++) {
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(i);
}
}
iterateWithDelay();
// Вывод каждую секунду:
// 0
// 1
// 2
// 3
// 4
Преимущества:
- Более читаемо и понятно
awaitпаузирует выполнение- Нет проблем с замыканиями
Вариант 4: Использование forEach с замыканием
const arr = [10, 20, 30, 40, 50];
arr.forEach((value, i) => {
setTimeout(() => {
console.log(i);
}, i * 1000);
});
Почему это работает:
forEachпередаёт индекс как параметр- Каждая итерация создаёт своё замыкание
- Проблемы с var не возникает
Вариант 5: Использование Array.from
const arr = [10, 20, 30, 40, 50];
Array.from({ length: arr.length }).forEach((_, i) => {
setTimeout(() => {
console.log(i);
}, i * 1000);
});
Демонстрация проблемы с var
НЕПРАВИЛЬНО — var в цикле:
const arr = [10, 20, 30, 40, 50];
for (var i = 0; i < arr.length; i++) {
setTimeout(() => {
console.log(i);
}, i * 1000);
}
// НЕПРАВИЛЬНЫЙ вывод:
// 5 (через 1 сек)
// 5 (через 2 сек)
// 5 (через 3 сек)
// 5 (через 4 сек)
// 5 (через 5 сек)
Почему выводит 5?
Шаг 1: var i = 0 (глобальная переменная)
Шаг 2: Цикл выполняется:
i = 0 → setTimeout(..., 0) → ссылка на i
i = 1 → setTimeout(..., 1000) → ссылка на i
i = 2 → setTimeout(..., 2000) → ссылка на i
i = 3 → setTimeout(..., 3000) → ссылка на i
i = 4 → setTimeout(..., 4000) → ссылка на i
i = 5 → цикл заканчивается
Шаг 3: Через 1 сек → console.log(i) → текущее значение i = 5
Шаг 4: Все setTimeout используют ОДНУ переменную i со значением 5
Ключевое различие:
var: одна переменная для всего цикла
let: новая переменная для каждой итерации
Объяснение замыканий
Замыкание (Closure) — это функция, которая имеет доступ к переменным из другой функции.
С var (ПЛОХО):
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // Все callbacks ссылаются на ОДНУ i
}, 1000);
}
// i = 3 → все выведут 3
С let (ХОРОШО):
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // Каждый callback ссылается на СВОЮ i
}, 1000);
}
// Выведет 0, 1, 2
Полный пример с использованием async/await
const arr = [10, 20, 30, 40, 50];
async function processArray() {
for (let i = 0; i < arr.length; i++) {
console.log(i);
// Ждём 1 секунду перед следующей итерацией
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
processArray().then(() => {
console.log('Готово!');
});
Более сложный пример: обработка массива значений
const arr = [10, 20, 30, 40, 50];
async function processWithDelay() {
for (let i = 0; i < arr.length; i++) {
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(`Index: ${i}, Value: ${arr[i]}`);
}
}
processWithDelay();
// Вывод каждую секунду:
// Index: 0, Value: 10
// Index: 1, Value: 20
// Index: 2, Value: 30
// Index: 3, Value: 40
// Index: 4, Value: 50
Тестирование
import { describe, it, expect, vi, beforeEach } from 'vitest';
describe('Асинхронная итерация', () => {
beforeEach(() => {
vi.useFakeTimers();
});
it('должен выводить правильные индексы с let', () => {
const logs: number[] = [];
const arr = [10, 20, 30, 40, 50];
for (let i = 0; i < arr.length; i++) {
setTimeout(() => {
logs.push(i);
}, i * 1000);
}
vi.advanceTimersByTime(4000);
expect(logs).toEqual([0, 1, 2, 3]);
});
it('должен работать с async/await', async () => {
const logs: number[] = [];
const arr = [10, 20, 30, 40, 50];
const processArray = async () => {
for (let i = 0; i < arr.length; i++) {
await new Promise(resolve => setTimeout(resolve, 1000));
logs.push(i);
}
};
processArray();
await vi.runAllTimersAsync();
expect(logs).toEqual([0, 1, 2, 3, 4]);
});
});
Сравнение подходов
| Способ | Простота | Читаемость | Современность |
|---|---|---|---|
| let в цикле | Простой | Понятно | Современно |
| IIFE с var | Средняя | Сложновато | Устаревший |
| async/await | Простой | Очень понятно | Очень современно |
| forEach | Простой | Хорошо | Современно |
Вывод
Используйте let в цикле for — это самый простой и современный подход:
for (let i = 0; i < arr.length; i++) {
setTimeout(() => {
console.log(i);
}, i * 1000);
}
Если нужна более читаемая асинхронная операция, используйте async/await:
async function iterate() {
for (let i = 0; i < arr.length; i++) {
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(i);
}
}