Learn JavaScript

Частые ошибки циклов в JavaScript

Циклы кажутся простыми, но ошибки в них встречаются очень часто. Обычно проблема в границах, неверном шаге или в том, что условие никогда не становится false. Ниже - самые распространенные случаи и способы их быстро заметить.

Бесконечный цикл

Бесконечный цикл - это цикл, который не заканчивается. Чаще всего причина одна: переменная, влияющая на условие, не меняется.

Забыли изменить счетчик

let i = 0;

// while (i < 3) {
//   console.log(i);
// } // i не меняется - бесконечный цикл

Исправление:

let i = 0;

while (i < 3) {
  console.log(i);
  i += 1;
}

Неправильный шаг

Если условие и шаг “смотрят в разные стороны”, цикл тоже может стать бесконечным.

let i = 0;

// while (i < 3) {
//   i -= 1;
// } // i уходит в минус, условие i < 3 всегда true

Исправление - согласовать условие и шаг:

let i = 0;

while (i < 3) {
  i += 1;
}

continue до изменения счетчика

В while это особенно опасно: continue может перепрыгнуть строку, где меняется счетчик.

let i = 0;

// while (i < 5) {
//   if (i === 2) continue;
//   i += 1;
// } // бесконечный цикл при i === 2

Исправление - менять i до continue:

let i = 0;

while (i < 5) {
  i += 1;
  if (i === 3) continue;
  console.log(i);
}

Ошибки границ (границы цикла)

i <= arr.length вместо i < arr.length

При переборе массива индексы идут от 0 до length - 1. Если поставить <=, появится лишняя итерация с undefined.

const items = ["a", "b", "c"];

for (let i = 0; i <= items.length; i += 1) {
  console.log(items[i]);
}
// "a", "b", "c", undefined

Правильно:

const items = ["a", "b", "c"];

for (let i = 0; i < items.length; i += 1) {
  console.log(items[i]);
}

Начали не с того индекса

Если нужен весь массив, старт почти всегда 0. Иногда цикл начинается с 1 и первый элемент пропускается.

const items = ["a", "b", "c"];

for (let i = 1; i < items.length; i += 1) {
  console.log(items[i]);
}
// "b", "c" (элемент "a" пропущен)

Off-by-one: ошибка на один шаг

Off-by-one - это ситуация, когда цикл делает на одну итерацию больше или меньше, чем нужно. Чаще всего это из-за < и <=, или из-за неверной стартовой/конечной точки.

Пример: нужно вывести числа 1, 2, 3, 4, 5.

Неверно:

for (let i = 1; i < 5; i += 1) {
  console.log(i);
}
// 1 2 3 4

Правильно:

for (let i = 1; i <= 5; i += 1) {
  console.log(i);
}
// 1 2 3 4 5

Изменение массива во время перебора

Если в цикле удаляются элементы из массива, индексы сдвигаются. В результате некоторые элементы могут быть пропущены.

Пример проблемы (удаление с splice во время прохода вперед):

const nums = [1, 2, 2, 3, 2];

for (let i = 0; i < nums.length; i += 1) {
  if (nums[i] === 2) {
    nums.splice(i, 1);
  }
}

console.log(nums); // может остаться [1, 2, 3] - один "2" пропущен

Более надежный подход - идти с конца:

const nums = [1, 2, 2, 3, 2];

for (let i = nums.length - 1; i >= 0; i -= 1) {
  if (nums[i] === 2) {
    nums.splice(i, 1);
  }
}

console.log(nums); // [1, 3]

Или не изменять массив, а создавать новый (часто так делают методами массивов).

Проблемы с for…of и изменением значений

for...of дает значение элемента, а не “ячейку массива”. Поэтому попытка изменить переменную цикла не изменит массив.

const nums = [1, 2, 3];

for (let n of nums) {
  n = n * 2;
}

console.log(nums); // [1, 2, 3]

Если нужно менять массив по месту, обычно используют for по индексам:

const nums = [1, 2, 3];

for (let i = 0; i < nums.length; i += 1) {
  nums[i] = nums[i] * 2;
}

console.log(nums); // [2, 4, 6]

Неправильное условие остановки

Иногда условие написано так, что цикл останавливается слишком рано или слишком поздно.

let i = 0;

while (i !== 3) {
  i += 2;
  // i будет 0, 2, 4, 6... и никогда не станет ровно 3
  if (i > 100) break;
}

В таких случаях обычно нужно переписать условие, чтобы оно зависело от диапазона:

let i = 0;

while (i < 3) {
  i += 2;
}

Что стоит запомнить

  • Бесконечный цикл почти всегда связан с тем, что условие не меняется или шаг неверный.
  • Для массива почти всегда используется i < arr.length.
  • Off-by-one чаще всего появляется из-за < и <= или неверной стартовой точки.
  • Если массив меняется внутри цикла, безопаснее идти с конца или не менять массив на месте.
  • В while нужно особенно внимательно использовать continue, чтобы не пропустить изменение счетчика.