JavaScript / 39 минут

Циклы и перебор данных

Стройте завершающиеся циклы и выбирайте правильный способ перебора.

В процессе Теория: впереди Практика: впереди Тест: впереди
06 / 22
После этой главы
  • Стройте завершающиеся циклы и выбирайте правильный способ перебора.
  • Применить тему в задаче «Циклы и перебор данных» и объяснить граничные случаи.

Около 9 минут на чтение и разбор примеров. Практика — отдельно, в своём темпе.

Повторение с понятным условием остановки

Цикл описывает повторяющееся действие: сложить числа, обработать строки заказа, найти первый подходящий элемент. До выбора синтаксиса определите три вещи: начальное состояние, условие продолжения и изменение состояния. Если ни одна операция не приближает цикл к остановке, программа может зависнуть. Особенно внимательно смотрите на ветви с continue: они тоже должны сохранять возможность завершения.

Для суммирования от 1 до n удобно хранить индекс текущего числа и аккумулятор суммы. До первой итерации индекс равен 1, сумма равна 0. Пока индекс не превышает n, число добавляется к сумме, после чего индекс растёт. При n=0 тело не выполнится ни разу. Правильный начальный аккумулятор позволяет обработать пустой диапазон без отдельного случайного исключения.

JavaScript
let total = 0;
for (let value = 1; value <= 4; value += 1) {
  total += value;
}
console.log(total); // 10

for: инициализация, проверка, шаг

Заголовок for содержит три части. Инициализация выполняется один раз, условие — перед каждой итерацией, шаг — после тела. В примере value += 1 сработает после добавления текущего числа. Это объясняет, почему границы нужно проверять отдельно: < и <= задают разные наборы чисел. Ошибка на единицу часто появляется на последнем элементе и остаётся незаметной на случайных больших входах.

Блочная переменная let существует в области цикла. Для обычного счётчика выбирайте имя, которое соответствует смыслу: index для позиции, day для номера дня, attempt для попытки. Короткое i нормально в небольшом локальном цикле, но несколько вложенных i, j и k без объяснений быстро мешают понять, какая координата изменяется. При вложенных циклах отдельно оцените количество итераций, а не только число строк.

while и do...while

while проверяет условие до выполнения тела. Он подходит, когда число повторений определяется состоянием: пока есть следующая порция данных или пока не найден ответ. do...while проверяет условие после тела и потому выполняется минимум один раз. Эта разница существенна для пустых данных: иногда попытка прочитать первый элемент до проверки уже является ошибкой.

JavaScript
let remaining = 3;
while (remaining > 0) {
  console.log(remaining);
  remaining -= 1;
}

let attempts = 0;
do {
  attempts += 1;
} while (attempts < 1);

Не используйте бесконечный while как способ ожидать сетевой ответ или событие браузера. Синхронное ожидание занимает поток и мешает обработке событий. Асинхронные операции строят через Promise, callbacks и соответствующие API среды. Пустое тело цикла не делает ожидание бесплатным и не позволяет интерфейсу продолжать работать.

for...of и for...in

for...of перебирает значения итерируемого объекта: элементы массива, code points строки, записи Map. Это хороший выбор, когда индекс не нужен. Если нужны и индекс, и значение массива, можно использовать entries(). for...in перебирает строковые имена перечислимых свойств, включая унаследованные перечислимые свойства. Он не является обычной заменой обхода массива.

JavaScript
const names = ['Аня', 'Олег'];
for (const name of names) console.log(name);
for (const [index, name] of names.entries()) {
  console.log(index, name);
}
for (const key of Object.keys({ js: 1, ts: 2 })) {
  console.log(key);
}

Выбор обхода должен отражать контракт данных. Если работаете с объектом-словарём, Object.keys, Object.values и Object.entries делают набор собственных данных явным. Если обрабатываете массив, обычно нужны его элементы, а не имена всех возможных свойств. Изменение массива в процессе обхода требует отдельного рассуждения: длина, порядок и ещё не посещённые элементы могут поменяться.

break, continue и ранний выход

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

JavaScript
function firstPositive(values) {
  for (const value of values) {
    if (value <= 0) continue;
    return value;
  }
  return null;
}

У вложенных циклов break по умолчанию относится только к ближайшему циклу. Не решайте сложность управления неочевидными переходами, если можно выделить поиск в отдельную функцию. Понятный локальный контракт обычно полезнее, чем попытка сохранить всё вычисление в одном большом блоке.

Практика и доказательство корректности

В задаче требуется сумма от 1 до n. Проверьте нулевой диапазон, один элемент и несколько элементов, для которых легко посчитать ответ вручную. Полезно записать таблицу: индекс до шага, сумма до шага, сумма после шага. Для n=3 она показывает последовательность 0→1→3→6 и помогает найти момент неправильного обновления.

Объясните инвариант: перед добавлением очередного числа аккумулятор содержит сумму уже пройденных чисел. Инициализация делает утверждение верным до первого шага, тело сохраняет его, а остановка означает, что все нужные числа учтены. Это простая форма рассуждения о программе, которую затем можно перенести на фильтрацию заказов, обход дерева и более сложные алгоритмы.

Загружаем редактор…

Музыка: Reverie — Scott Buckley · CC BY 4.0