Циклы в JavaScript: for, while, for…of, for…in и forEach с примерами

Циклы в JavaScript: for, while, for...of, for...in и forEach с примерами Полезное

Цикл в JavaScript — это конструкция, которая повторяет блок кода: либо пока условие истинно (for, while, do...while), либо по одному разу для каждого элемента коллекции (for...of) или каждого ключа объекта (for...in). Отдельно стоит метод массива forEach — это не оператор цикла, а функция, которая сама вызывает ваш колбэк для каждого элемента.

Ниже — таблица выбора, каждый вид цикла на запускаемом примере, управление через break, continue и метки, и ошибки, на которых чаще всего спотыкаются. Все примеры проверены 24.09.2026 в Node.js 24.21 (LTS); в современных браузерах поведение то же.

Три термина, которые путают

  • Итерация — один проход тела цикла. Цикл из 4 итераций выполняет тело 4 раза.
  • Итерируемый объект — то, что умеет отдавать значения по очереди через Symbol.iterator: массив, строка, Map, Set, NodeList. По нему ходит for...of.
  • Перечисляемое свойство — ключ объекта с флагом enumerable. По таким ключам (включая унаследованные) ходит for...in.

Итерируемый и перечисляемый — разные вещи: обычный объект { a: 1 } перечисляемый, но не итерируемый, поэтому for...in по нему работает, а for...of падает.

Какой цикл выбрать

Задача Что взять Когда иначе
Нужен индекс или шаг не 1 for (let i = 0; ...; i++) если индекс не нужен — for...of
Перебрать значения массива, строки, Map, Set for...of нужен индекс — arr.entries() или for
Повторять, пока не выполнится условие, число шагов неизвестно while тело должно выполниться хотя бы раз — do...while
Перебрать ключи и значения обычного объекта for...of + Object.entries(obj) for...in — если нужны и унаследованные ключи
Выполнить действие для каждого элемента без досрочного выхода forEach нужен break или await — for...of

for: цикл со счетчиком

Минимальный полный пример — сумма чеков с выводом каждого шага:

const prices = [120, 450, 80, 990];
let total = 0;

for (let i = 0; i < prices.length; i++) {
  total += prices[i];
  console.log(`шаг ${i}: +${prices[i]}, итого ${total}`);
}

console.log('сумма:', total);
шаг 0: +120, итого 120
шаг 1: +450, итого 570
шаг 2: +80, итого 650
шаг 3: +990, итого 1640
сумма: 1640

В заголовке for три части через точку с запятой. Порядок выполнения такой:

  1. Инициализация let i = 0 — один раз перед циклом.
  2. Условие i < prices.length — перед каждой итерацией; если false, цикл завершается.
  3. Тело — блок в фигурных скобках.
  4. Шаг i++ — после тела, затем снова пункт 2.

Все три части необязательны: for (;;) { ... } — бесконечный цикл, из которого выходят через break. Фигурные скобки при одной инструкции в теле можно опустить, но в рабочем коде их обычно ставят: так меньше ошибок при добавлении второй строки.

let или var в счетчике

Объявлять счетчик через let, а не var, важно не только из вкуса. let создает новую переменную на каждую итерацию, var — одну на всю функцию. Разница видна, когда внутри цикла создаются функции:

const fns = [];
for (var i = 0; i < 3; i++) {
  fns.push(() => i);
}
console.log(fns.map((f) => f()));

const fixed = [];
for (let j = 0; j < 3; j++) {
  fixed.push(() => j);
}
console.log(fixed.map((f) => f()));
[ 3, 3, 3 ]
[ 0, 1, 2 ]

С var все три функции видят одну переменную, которая к моменту вызова уже равна 3. С let каждая функция запомнила свое значение.

while: пока условие истинно

while нужен, когда число повторений заранее неизвестно. Пример: за сколько месяцев вклад в 1000 при росте 10% в месяц превысит 2000 (учебная модель, не финансовый расчет):

let balance = 1000;
let months = 0;

while (balance < 2000) {
  balance *= 1.1;
  months++;
}

console.log(months, balance.toFixed(2));
8 2143.59

Условие проверяется перед каждой итерацией. Если оно ложно с самого начала, тело не выполнится ни разу. Главный риск while — забыть изменить переменную из условия: тогда цикл станет бесконечным, и вкладка браузера или процесс Node.js зависнет.

do…while: хотя бы один раз

У do...while проверка стоит после тела, поэтому тело выполняется минимум один раз. Сравнение на одном и том же ложном условии:

let n = 10;

while (n < 5) {
  console.log('while:', n);
}

do {
  console.log('do...while:', n);
} while (n < 5);
do...while: 10

while не выполнился ни разу, do...while — один раз. Типичный сценарий: запросить ввод и повторять запрос, пока он некорректен. После while (...) в do...while принято ставить точку с запятой. Строго она не обязательна: для do...while в спецификации есть отдельное правило автоподстановки, и код без нее выполнится, но явная точка с запятой снимает вопросы при чтении.

for…of и for…in: значения против ключей

Это самая частая путаница. Коротко: for...of перебирает значения итерируемого объекта, for...in — имена перечисляемых свойств, включая унаследованные, причем ключи всегда строки.

const langs = ['js', 'ts', 'py'];
langs.extra = 'свойство';
Array.prototype.bad = function () {};

for (const key in langs) {
  console.log('for...in:', key, typeof key);
}

for (const value of langs) {
  console.log('for...of:', value);
}

delete Array.prototype.bad;
for...in: 0 string
for...in: 1 string
for...in: 2 string
for...in: extra string
for...in: bad string
for...of: js
for...of: ts
for...of: py

for...in зацепил пользовательское свойство extra и метод bad, добавленный в прототип (в реальном коде так делать не надо — здесь это демонстрация). for...of прошел ровно по трем элементам. Вывод: для массивов — for...of или обычный for, а for...in — для объектов.

Порядок ключей в for...in тоже не порядок записи: ключи-индексы массива (неотрицательные целые от 0 до 4294967294) идут первыми по возрастанию, затем остальные строковые в порядке добавления. Для { b: 1, 2: 'two', a: 2, 1: 'one' } получится 1, 2, b, a. Ключи '-1' или '4294967295' индексами не считаются и стоят в порядке добавления, как обычные строки.

Как перебрать обычный объект

Надежный способ — Object.entries: он берет только собственные перечисляемые свойства и отдает пары ключ-значение, которые удобно разобрать в for...of:

const user = { name: 'Анна', age: 30 };

for (const [key, value] of Object.entries(user)) {
  console.log(`${key} = ${value}`);
}

try {
  for (const x of user) console.log(x);
} catch (e) {
  console.log(e.constructor.name + ': ' + e.message);
}
name = Анна
age = 30
TypeError: user is not iterable

Вторая часть показывает, что будет, если применить for...of к объекту напрямую: ошибка TypeError, потому что у объекта нет Symbol.iterator.

Строки: for…of видит символы, индекс — кодовые единицы

Строка в JavaScript — это последовательность 16-битных кодовых единиц UTF-16. Символы за пределами базовой плоскости Unicode, например эмодзи 😀, занимают две единицы (суррогатную пару), и цикл по индексу разрежет такой символ пополам:

const word = 'Hi😀';

for (let i = 0; i < word.length; i++) {
  console.log('index', i, JSON.stringify(word[i]));
}

for (const ch of word) {
  console.log('of', ch);
}
console.log(word.length, [...word].length);
index 0 "H"
index 1 "i"
index 2 "\ud83d"
index 3 "\ude00"
of H
of i
of 😀
4 3

for...of идет по кодовым точкам, поэтому эмодзи целый. Граница: сложные эмодзи из нескольких кодовых точек (флаги, семья, эмодзи с оттенком кожи) for...of тоже разделит — для видимых символов нужен Intl.Segmenter.

forEach: метод, а не цикл

arr.forEach(fn) вызывает функцию для каждого элемента. Это удобно, но у метода два ограничения, которых нет у операторов цикла. Первое — из него нельзя выйти через break (это синтаксическая ошибка), а return внутри колбэка лишь пропускает текущий элемент, как continue:

const nums = [5, 12, 8, 130, 44];

nums.forEach((n, i) => {
  if (n > 10) return;
  console.log('forEach:', i, n);
});

for (const n of nums) {
  if (n > 100) break;
  console.log('for...of:', n);
}
forEach: 0 5
forEach: 2 8
for...of: 5
for...of: 12
for...of: 8

Второе — forEach не ждет асинхронные колбэки. Если внутри await, код после forEach выполнится раньше, чем закончатся операции:

const wait = (ms, v) => new Promise((r) => setTimeout(() => r(v), ms));

async function main() {
  [1, 2].forEach(async (n) => {
    await wait(10);
    console.log('forEach готово:', n);
  });
  console.log('после forEach');

  for (const n of [1, 2]) {
    await wait(10);
    console.log('for...of готово:', n);
  }
  console.log('после for...of');
}

main();
после forEach
forEach готово: 1
forEach готово: 2
for...of готово: 1
for...of готово: 2
после for...of

Для последовательной асинхронной обработки берут for...of с await. Если нужен новый массив из старого, вместо цикла с push обычно используют map или filter — это уже тема методов массивов, а не циклов.

break, continue и метки

break полностью завершает ближайший цикл, continue пропускает остаток текущей итерации и переходит к следующей. Во вложенных циклах break выходит только из внутреннего. Чтобы выйти сразу из обоих, ставят метку — идентификатор с двоеточием перед внешним циклом:

const matrix = [
  [3, 8, 1],
  [7, -2, 5],
  [4, 6, -9],
];

outer: for (let row = 0; row < matrix.length; row++) {
  for (let col = 0; col < matrix[row].length; col++) {
    if (matrix[row][col] < 0) {
      console.log(`первое отрицательное: ${matrix[row][col]} в [${row}][${col}]`);
      break outer;
    }
  }
}

for (const n of [1, 2, 3, 4, 5, 6]) {
  if (n % 2 === 0) continue;
  console.log('нечетное:', n);
}
первое отрицательное: -2 в [1][1]
нечетное: 1
нечетное: 3
нечетное: 5

Без метки break вышел бы только из внутреннего цикла, и поиск продолжился бы на третьей строке, найдя еще -9. Метки работают с for, while, do...while, for...of, for...in, но не с forEach. Если метка понадобилась в глубокой вложенности, часто читаемее вынести поиск в функцию и выйти через return.

Типичные ошибки

Выход за границу массива. Условие <= вместо < дает лишнюю итерацию:

const arr = [10, 20, 30];
for (let i = 0; i <= arr.length; i++) {
  console.log(i, arr[i]);
}
0 10
1 20
2 30
3 undefined

Ошибки нет, но появился undefined, который дальше может испортить расчет. Исправление: i < arr.length.

Арифметика с ключом из for…in. Ключи — строки, поэтому + склеивает, а не складывает:

const ids = [1, 2, 3];
for (const i in ids) {
  console.log(i + 1);
}
01
11
21

Исправление: для массива — for (let i = 0; i < ids.length; i++) или for (const [i, v] of ids.entries()), где индекс уже число.

Удаление элементов во время прохода вперед. После splice элементы сдвигаются, и следующий пропускается:

const list = [1, 2, 2, 3];
for (let i = 0; i < list.length; i++) {
  if (list[i] === 2) list.splice(i, 1);
}
console.log(list);

const list2 = [1, 2, 2, 3];
for (let i = list2.length - 1; i >= 0; i--) {
  if (list2[i] === 2) list2.splice(i, 1);
}
console.log(list2);
console.log([1, 2, 2, 3].filter((x) => x !== 2));
[ 1, 2, 3 ]
[ 1, 3 ]
[ 1, 3 ]

Первый вариант оставил одну двойку. Исправление — идти с конца или не менять массив, а построить новый через filter.

Бесконечный цикл. Если в while условие никогда не становится ложным, выполнение не завершится. Симптом — зависшая вкладка или процесс, который не отвечает. В браузере помогает закрыть вкладку, в Node.js — Ctrl+C; затем проверить, меняется ли в теле переменная из условия и достижимо ли условие выхода.

Выводы

  • for — когда нужен индекс или нестандартный шаг; счетчик объявляйте через let.
  • while — число шагов неизвестно; do...while — тело должно выполниться хотя бы раз.
  • for...of перебирает значения итерируемых объектов, for...in — строковые ключи, включая унаследованные; для массивов for...in не подходит.
  • forEach нельзя прервать break и он не ждет await; для досрочного выхода и асинхронности берите for...of.
  • Метка с break выходит сразу из нескольких вложенных циклов.

Где применяется / связь с практикой

Циклы в JavaScript встречаются везде: обработка ответа API, отрисовка списка товаров, валидация полей формы, обход DOM-элементов через NodeList. На практике важнее не синтаксис, а выбор: какой цикл не зацепит лишние ключи, где нужен досрочный выход и как не потерять порядок асинхронных операций.

Освойте тему на практике

Если хотите системно разобраться с основами языка на задачах, посмотрите курс «JavaScript-разработчик. Базовый уровень». Бесплатно познакомиться с форматом можно на открытых уроках Otus.

FAQ

Какой цикл в JavaScript самый быстрый?
Универсального ответа нет: разница между for, for...of и forEach зависит от движка, версии и данных и в обычном коде обычно незаметна. Выбирайте по читаемости, а скорость измеряйте на своем сценарии.

Можно ли перебрать Map или Set через for…in?
Нет, их элементы не являются перечисляемыми свойствами: ошибки не будет, цикл просто не выполнится ни разу. Используйте for...of: для Map он отдает пары [ключ, значение], для Set — значения.

Как получить индекс в for…of?
Через arr.entries(): for (const [i, value] of arr.entries()). Индекс при этом будет числом, а не строкой, как в for...in.

OTUS Журнал
Бесплатные открытые уроки (поп-ап)