Всё о циклах в JavaScript

Источник: «All About JavaScript Loops»
В каждом языке программирования есть циклы. Циклы выполняют операцию (т. е. часть работы) несколько раз, обычно по одному разу для каждого элемента массива или списка, или просто повторяют операцию до тех пор, пока не будет выполнено определённое условие.

В JavaScript, есть довольно много типов циклов. Я даже не все из них использовал, поэтому ради собственного любопытства решил сделать их поверхностный обзор. И, как оказалось, есть довольно веские причины, не использовать пару из этих типов.

Итак, давайте изучим различные типы циклов, что можно делать с каждым из них и почему следует использовать один, а не другой. (К концу вам покажется, что эта небольшая игра слов совершенно уморительна).

Циклы while и do...while

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

// удаление первого элемента из массива и запись его в лог до тех пор, пока массив не станет пустым
let queue1 = ["a", "b", "c"];

while (queue1.length) {
let item = queue1.shift();

console.log(item);
}

// то же самое, что и выше, но также записывается в лог, когда массив пуст
let queue2 = [];

do {
let item = queue2.shift() ?? "empty";

console.log(item);
} while (queue2.length);

Цикл for

Далее следует цикл for. Он используется для того, чтобы сделать что-то определённое количество раз. Если необходимо повторить операцию, скажем, 10 раз, используйте цикл for. Этот цикл может быть пугающим для новичков в программировании, но переписывание того же цикла в стиле while поможет проиллюстрировать синтаксис и закрепить его в памяти.

// выводим числа от 1 до 5
for (let i = 1; i <= 5; i++) {
console.log(i);
}

// то же самое, но в виде цикла while
let i = 1; // первая часть цикла for

// вторая
while (i <= 5) {
console.log(i);

i++; // третья
}

Циклы for...of и for await...of

Цикл for...of — самый простой способ просмотреть массив.

let myList = ["a", "b", "c"];

for (let item of myList) {
console.log(item);
}

Однако они не ограничиваются массивами. Технически они могут итерировать всё, что реализует так называемый iterable protocol (протокол перебора). Есть несколько встроенных типов, реализующих этот протокол: array, maps, set и string, и это только самые распространённые, но можно реализовать протокол и в собственном коде. Для этого нужно добавить метод [Symbol.iterator] к любому объекту, и этот метод должен возвращать итератор. Это немного запутанно, но суть в том, что итераторы — это объекты со специальным методом, возвращающим итераторы; фабричный метод для итераторов, если хотите. Специальный тип функции, называемый генератор, — это функция, возвращающая и итерируемый, и итератор:

let myList = {
*[Symbol.iterator]() {
yield "a";
yield "b";
yield "c";
},
};

for (let item of myList) {
console.log(item);
}

Существует асинхронная версия всех тех вещей, которые были упомянуты: async итерируемые, async итераторы и async генераторы. Можно использовать async итерируемый с for await...of.

async function delay(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}

// на этот раз мы создаём не итерируемый, а генератор
async function* aNumberAMinute() {
let i = 0;

while (true) {
// бесконечный цикл
yield i++;

// минутная пауза
await delay(60_000);
}
}

// это генератор, поэтому нужно вызвать его самостоятельно
for await (let i of aNumberAMinute()) {
console.log(i);

// остановить через один час
if (i >= 59) {
break;
}
}

Одна из неочевидных особенностей оператора for await...of заключается в том, что его можно использовать с неасинхронными итерируемыми, и он будет прекрасно работать. Обратное, однако, не верно: нельзя использовать асинхронные итерируемые с оператором for...of.

Циклы forEach и map

Хотя технически это не циклы как таковые, их можно использовать для итерации по списку.

Вот что касается метода forEach. Исторически он был намного медленнее, чем использование цикла for. Думаю, в некоторых случаях это уже не так, но если беспокоит производительность, то лучше его не использовать. А теперь, когда есть for... of, не уверен, что осталось много причин для его использования. Думаю, единственная причина, по которой он всё ещё может использоваться, — это если есть готовая функция, которую можно использовать в качестве обратного вызова, но её можно просто вызвать из тела for...of.

Однако forEach также получает индекс для каждого элемента, так что это тоже может пригодиться. В конечном счёте, решение об использовании forEach будет зависеть от того, использует ли его какой-либо другой код, с которым приходится работать, но лучше избегать его использования.

let myList = ["a", "b", "c"];

for (let item of myList) {
console.log(item);
}

// но, возможно, если нужен индекс, используйте forEach
["a", "b", "c"].forEach((item, index) => {
console.log(`${index}: ${item}`);
});

Между тем map, по сути, преобразует один массив в другой. Он всё ещё имеет то же влияние на производительность, что и forEach, но читать его немного приятнее, чем альтернативу. Впрочем, это, конечно, субъективно, и, как и в случае с forEach, можно делать то, что делает весь остальной ваш код. Его часто можно встретить в React и библиотеках, вдохновлённых React, как основной способ циклического просмотра массива и вывода списка элементов в JSX.

function MyList({items}) {
return (
<ul>
{items.map((item) => {
return <li>{item}</li>;
})}
</ul>
);
}

Цикл for...in

Список циклов в JavaScript был бы неполным без упоминания оператора for...in, потому что он может перебирать поля объекта. Однако он также просматривает поля, унаследованные через цепочку прототипов объекта, и по этой причине я, честно говоря, всегда избегал его.

Тем не менее, если есть литеральный объекта, то for...in может быть приемлемым способом итерации по ключам этого объекта. Также стоит отметить, что если вы давно программируете на JavaScript, то, возможно, помните, что раньше порядок ключей в разных браузерах был непоследовательным, но теперь порядок последовательный. Любой ключ, который может быть индексом массива (т. е. положительные целые числа), будет первым по возрастанию, а затем все остальные в порядке указанном автором.

let myObject = {
a: 1,
b: 2,
c: 3,
};

for (let k in myObject) {
console.log(myObject[k]);
}

Подведение итогов

Циклы — это то, что многие программисты используют каждый день, хотя их можно воспринимать как должное или не слишком задумываться о них.

Но если отступить на шаг назад и посмотреть на все способы, которыми можно перебирать объекты в JavaScript, то окажется, что существует несколько способов сделать это. Мало того, между ними есть существенные — если не сказать нюансные — различия, которые могут и будут влиять на подход к скриптам.

Дополнительные материалы

Предыдущая Статья

PHP 8.4 Property Hooks (хуки свойств)

Следующая Статья

"Умные" макеты с контейнерными запросами