Основы циклов в TypeScript: while, do-while, for, break и continue
Узнайте, как циклы while, do-while и for в TypeScript оценивают свои условия, почему изменение области видимости переменной цикла влияет на результаты, и как операторы break и continue изменяют ход выполнения кода.
Источники: Mastering Loops in TypeScript: while, do-while, for, break & continue
Циклы позволяют выполнять один и тот же блок кода неоднократно до тех пор, пока выполняется определённое условие, вместо того чтобы постоянно копировать одно и то же предложение. Они кажутся простыми, но большинство ошибок в циклах возникают из-за трёх аспектов: момента проверки условия, местоположения переменной цикла и расположения команд break или continue внутри тела цикла. В этом руководстве рассматриваются циклы while, do-while и for в TypeScript с простыми примерами, результаты выполнения которых можно предсказать по шагам, чтобы вы могли читать и писать код циклических операций без неожиданностей.
Почему циклический подход лучше повторения
Чтобы вывести числа от 1 до 10 без использования цикла, потребуется десять вызовов console.log. С помощью цикла вы описываете работу один раз, а среда выполнения повторяет её; размер кода остаётся прежним независимо от того, нужно ли десять или десять тысяч повторений.
Цикл while: сначала проверка, затем выполнение
Цикл while имеет самую простую структуру из всех:
while (condition) {
// statements
}
Условие оценивается до каждой итерации. Пока оно равно true, выполняется тело цикла; как только оно становится false, цикл завершается. Чтобы цикл while корректно работал, необходимы три элемента:
- начальное значение переменной, от которой зависит условие;
- само условие;
- что-то в теле цикла, что изменяет значение этой переменной так, чтобы условие стало
false.
Если пропустить третий элемент, цикл никогда не остановится — об этой проблеме речь пойдет ниже.
Счисление от 1 до 10
Здесь видны все три элемента: переменная i начинает с значения 1, цикл выполняется до тех пор, пока i <= 10, а операция i++ увеличивает ее значение после каждой итерации:
let i: number = 1; // initialization
while (i <= 10) {
console.log(i);
i++;
}
Фильтрация четных чисел
Чтобы вывести только четные числа в этом диапазоне, продолжайте перебирать каждое значение, но добавьте проверку внутри цикла. Оператор остатка от деления % возвращает остаток после деления, поэтому i % 2 == 0 истинно тогда, когда i — четное число:
let i: number = 1;
while (i <= 10) {
if (i % 2 == 0) {
console.log(i);
}
i++;
}
Существует более эффективный способ достижения похожего результата: полностью пропустите проверку и с каждым разом увеличивайте значение на два. Обратите внимание, что в этой версии начало счисления — 0, поэтому в результате также будет содержаться 0:
let i: number = 0;
while (i <= 10) {
console.log(i);
i = i + 2;
}
Результат: 0, 2, 4, 6, 8, 10
Фильтрация нечетных чисел
Если изменить условие на != 0, будут выбираться нечетные числа. Начинать с 0 здесь безопасно, поскольку 0 является четным числом и будет отфильтровано:
let i: number = 0;
while (i <= 10) {
if (i % 2 != 0) {
console.log(i);
}
i++;
}
Результат: 1, 3, 5, 7, 9
В этих примерах используются операторы == и !=; обычно предпочитаются строгие формы === и !==, поскольку они никогда не принудительно не преобразуют типы, хотя для двух чисел результат будет одинаковым.
Отсчёт назад
Чтобы отсчитывать назад, начните с большого значения и уменьшайте его, пока значение будет равно по меньшей мере 1:
let i: number = 10;
while (i >= 1) {
console.log(i);
i--;
}
Бесконечные циклы и причины их возникновения
Условие цикла while — это просто выражение, возвращающее логическое значение, поэтому while (true) является совершенно корректным. Приведённый ниже цикл будет выводить данные бесконечно, поскольку его невозможно прервать:
let i: number = 1;
while (true) {
console.log(i);
i++;
// no break, no exit condition this runs forever
}
Чаще всего бесконечные циклы возникают случайно: переменная управления никогда не обновляется или меняет значения в неправильном направлении. Неконтролируемый цикл блокирует всё остальное в своей нити, из-за чего вкладка браузера или процесс Node.js кажутся зависшими. Намеренное использование конструкции while (true) допустимо, если предусмотрен явный способ выхода, обычно с помощью ключевого слова break (описано ниже), например, в логике «повторять попытки до успеха».
Цикл do-while: выполнение один раз, затем проверка
В цикле do-while условие проверяется в конце:
do {
// statements
} while (condition);
Поскольку проверка происходит после выполнения тела цикла, тело всегда выполняется хотя бы один раз, независимо от значения условия. В отличие от этого, обычный цикл while может не выполняться вовсе.
Когда условие изначально истинно, цикл do-while ведет себя точно так же, как и цикл while:
let i: number = 1;
do {
console.log(i);
i++;
} while (i <= 5);
Разница проявляется, когда условие с самого начала является ложным:
let i: number = 10;
do {
console.log(i);
i++;
} while (i <= 5);
Результат: 10
Тело цикла выполняется безусловно, выводится 10, а переменная i увеличивается до 11. Только затем оценивается условие i <= 5; оно имеет значение false, поэтому цикл завершается после одной итерации. Цикл while (i <= 5), начинающийся с значения 10, ничего не выведет. Используйте do-while, когда первая итерация необходима в любом случае, например, для отображения запроса хотя бы один раз.
Цикл for: всё в одном заголовке
Цикл for — это тот, который вы будете использовать чаще всего, особенно когда заранее известно, сколько раз нужно повторить операции или насколько должен изменяться счётчик. Он объединяет инициализацию, условие и обновление в один заголовок, что делает его более компактным по сравнению с аналогичным циклом while:
for (initialization; condition; increment/decrement) {
// statements
}
Следует запомнить порядок выполнения этих этапов:
- Инициализация происходит один раз, до любых других действий (например
let i = 1). - Оценивается условие. Если оно равно
true, выполняется тело цикла; еслиfalse, цикл завершается. - После тела цикла выполняется операция обновления (например
i++). - Управление возвращается к шагу 2, и цикл повторяется до тех пор, пока условие не станет
false.
Другими словами, инициализация происходит один раз, в то время как проверка условия и обновление данных выполняются при каждом цикле.
Счёт от 1 до 10
for (let i = 1; i <= 10; i++) {
console.log(i);
}
Два способа получения чётных чисел
Как и в случае с while, можно делать шаг по два:
for (let i = 2; i <= 10; i += 2) {
console.log(i);
}
или обрабатывать каждое значение и фильтровать их внутри тела цикла:
for (let i = 2; i <= 10; i++) {
if (i % 2 == 0) {
console.log(i);
}
}
Оба способа выводят одну и ту же последовательность:
2
4
6
8
10
Метод шагового перебора выполняет меньше операций, поскольку обрабатывает только чётные значения; фильтрация более гибка, когда правило не является фиксированным шагом, например «делится на 3 или 7».
Обратный отсчёт
Обновление путём уменьшения значения заставляет цикл выполняться в обратном порядке:
for (let i = 10; i >= 1; i--) {
console.log(i);
}
Результат: 10, 9, 8, 7, 6, 5, 4, 3, 2, 1
Почему переменная цикла равна 6 после завершения цикла
Место объявления переменной цикла влияет на её значение после завершения цикла. В этом примере i объявляется до начала цикла, а блок for лишь присваивает ей значение:
let i: number;
for (i = 1; i <= 5; i++) {
console.log(i);
}
console.log(i);
Результат: 1, 2, 3, 4, 5, 6
Поскольку i находится во внешнем области видимости, последняя команда console.log(i) всё равно может к ней обратиться, сохраняя значение, оставленное циклом. Это число 6, а не 5, из-за порядка выполнения операций: в последнем проходе i равно 5, тело цикла выводит это значение, затем происходит его увеличение до 6, и только тогда условие 6 <= 5 становится ложным, что приводит к завершению цикла.
Более распространённый стиль — for (let i = 1; i <= 5; i++) — предоставляет i в блоковой области видимости, ограниченной циклом, поэтому после его завершения оно больше не существует. Обращение к нему после цикла вызывает ошибку компиляции в TypeScript («Cannot find name 'i'»), а в обычном JavaScript приводит к возникновению ошибки ReferenceError во время выполнения. Обычно лучше объявлять переменную в начале кода; использовать внешнее объявление следует только тогда, когда необходимо получить окончательное значение.
Изменение хода выполнения с помощью break и continue
Два оператора прерывания позволяют изменить обычный ход цикла прямо внутри его тела:
breakнемедленно завершает цикл, независимо от того, сколько итераций ещё предстоит выполнить.continueпрерывает только текущую итерацию и переходит к следующей; цикл продолжает работать.
В цикле for оператор continue всё равно выполняет выражение обновления перед повторной проверкой условия, поэтому счётчик продолжает увеличиваться.
break: важна позиция внутри тела цикла
Здесь проверка break происходит до вызова функции вывода:
let i: number;
for (i = 1; i <= 10; i++) {
if (i == 5) {
break;
}
console.log(i);
}
Результат: 1, 2, 3, 4
Когда значение i достигает 5, цикл завершается до того, как успевает выполниться console.log, поэтому число 5 не отображается.
Если переместить операцию вывода выше проверки, результат изменится:
let i: number;
for (i = 1; i <= 10; i++) {
console.log(i);
if (i == 5) {
break;
}
}
Результат: 1, 2, 3, 4, 5
Условие осталось прежним, но теперь отображается число 5, потому что операция вывода выполняется до проверки условия. Как и в случае с break, так и с continue, порядок выполнения операций внутри цикла определяет результат.
continue: пропуск одного значения
Замена break на continue превращает действие «остановиться на 5» в действие «пропустить 5»:
let i: number;
for (i = 1; i <= 10; i++) {
if (i == 5) {
continue;
}
console.log(i);
}
Результат: 1, 2, 3, 4, 6, 7, 8, 9, 10
Цикл продолжает работу; просто для этой итерации не выполняется операция вывода.
Пропуск всех нечётных чисел
В сочетании с фильтром continue направляет каждое нечётное значение сразу к следующей итерации:
let i: number;
for (i = 0; i <= 10; i++) {
if (i % 2 != 0) {
continue;
}
console.log(i);
}
Результат: 0, 2, 4, 6, 8, 10
Пропуск списка определённых значений
Условие может быть настолько сложным, насколько это необходимо. Используя логический оператор OR ||, этот цикл пропускает значения 3, 5 и 7:
let i: number;
for (i = 0; i <= 10; i++) {
if (i == 3 || i == 5 || i == 7) {
continue;
}
console.log(i);
}
Результат: 0, 1, 2, 4, 6, 8, 9, 10
Для более длинного списка значений для пропуска лучше использовать [3, 5, 7].includes(i) или структуру Set.
Основные выводы
- В цикле
whileусловие проверяется перед каждым проходом, и он может не выполниться ни разу; в циклеdo-whileпроверка происходит после выполнения кода, поэтому он всегда выполняется хотя бы один раз. - Каждый цикл требует обновления, которое в конечном итоге сделает условие ложным; в противном случае возникнет бесконечный цикл, блокирующий поток.
- В цикле
forинициализация выполняется один раз, в то время как проверка условия и обновление происходят при каждом цикле, поэтому счётчик в итоге превышает установленный лимит на один шаг.
let в начале цикла ограничивает его действие только этим циклом; объявление вне цикла позволяет его окончательному значению сохраняться.break прерывает цикл, а continue пропускает одну итерацию; место их размещения относительно других операторов влияет на результат выполнения.Условные операторы определяют, какой код будет выполнен; циклы определяют частоту его выполнения. Вместе они покрывают большую часть схем управления, которые вы будете использовать.