5.01. Циклы в JavaScript
Циклы в JavaScript
Что такое цикл?
★ Цикл – многократно повторяемая процедура. Она может быть с известным количеством итераций и с неопределённым (бесконечным). То есть, либо мы знаем, когда начинать, сколько раз повторить, либо повторяем снова и снова, пока что-то не изменится.
for
for используется для определённого количества операций, с указанием начального значения, условия и шага каждой итерации:
for (начало; условие; шаг) {
// Тело цикла
}
Так, это похоже на функцию, где есть три выражения:
- начало задаёт начальное состояние цикла. Обычно здесь объявляется и инициализируется счётчик, который выполняется один раз перед началом. В начале можно как объявить переменную, так и использовать существующую;
- условие, которое проверяется перед каждой итерацией. Если это условие true (истинно), цикл выполняется, если false – цикл останавливается. Если условие пропустить, цикл будет выполняться бесконечно. Тут можно использовать любые логические выражения (
<,>,<=,=>и т.д.); - шаг, который выполняется после каждой итерации и меняет счётчик.
Пример:
for (let i = 0; i < 5; i++) {
console.log(i); // Выведет 0, 1, 2, 3, 4
}
Как это работает?
for– ключевое слово, указывающее на начало цикла;let i = 0– это начало, создание переменной с именем i и со значением 0;i < 5– это условие, и еслиi < 5то будет true и тело цикла выполняется;console.log(i)– это тело цикла, которое выводит в консоль значение переменнойi;i++- инкремент, когда значение увеличивается на 1;- повторная проверка будет выполняться снова и снова;
- если проверка true – цикл запускается снова, и снова выполняется тело.
Шаг — это выражение, которое изменяет значение переменной-счётчика на каждой итерации. Обычно он стоит на третьем месте в заголовке цикла for.
Как можно описать шаг?
| Шаг | Описание |
|---|---|
i++ | увеличивает i на 1 |
i-- | уменьшает i на 1 |
i += 2 | увеличивает i на 2 |
i -= 3 | уменьшает i на 3 |
i *= 2 | умножает i на 2 |
i /= 10 | делит i на 10 |
++ и -— это инкремент и декремент. Это специальные операторы для увеличения или уменьшения значения на 1. Бывают пост- и пре- операторы:
i++это постинкремент;++iэто преинкремент;i-—это постдекремент;--iэто предекремент. Разница между пре- и постформами важна только если результат используется сразу.
Большинство этих операций (++, --, +=, -=, *=, /=) работают с числами , но JavaScript достаточно гибкий, и эти операторы могут работать и с другими типами данных.
Основное использование, конечно, числа, к примеру x+=3.
Со строками работает только +=, оператор работает как конкатенация строк:
let str = 'Привет';
str += ', мир!'; // 'Привет, мир!'
Но ++, --, -=, *= и т.д. со строками работать не будут корректно , так как JS попытается привести строку к числу, что может привести к NaN.
С объектами и массивами такие операции не имеют смысла. Поэтому грамотно подходите к выбору циклов.
forEach
Для работы с массивами лучше использовать forEach (в других языках он является полноценным циклом, тогда как в JavaScript это просто метод объектов-массивов), который озволяет перебрать все элементы массива , выполнив для каждого элемента заданную функцию (т.н. callback-функцию).
Он удобен и читабелен, особенно когда не нужно управлять индексами вручную, как в классическом for. Синтаксис у него таков:
array.forEach(function(element, index, array) {
// тело функции
});
Здесь мы вызываем функцию с параметрами:
- element - текущий элемент массива, обязательный;
- index - индекс текущего элемента (начинается с 0);
- array - сам массив, по которому идёт перебор.
Пример:
const fruits = ['яблоко', 'банан', 'груша'];
fruits.forEach(function(fruit) {
console.log(fruit);
});
// Вывод:
// яблоко
// банан
// груша
Функцию можно сделать стрелочной:
const numbers = [1, 2, 3];
numbers.forEach(num => console.log(num * 2));
// Вывод:
// 2
// 4
// 6
forEach не мутирует (не изменяет) исходный массив, break здесь не нужен (он его просто не поддерживает), а код будет более читабельным. Так что можно сказать, что в JS есть цикл foreach, но он просто метод для работы с массивами, для перебора элементов.
while
while используется, когда количество итераций неизвестно. Код будет выполняться то тех пор, пока условие true.
let i = 0;
while (i < 5) {
console.log(i); // Выведет 0, 1, 2, 3, 4
i++;
}
В данном случае, пока i не будет равным 5, будет выводиться значение i в консоль. Принцип такой же, как и в for, однако он не включает в себя объявление и цикл – всё, что нужно выполнять, выведено в тело цикла, в том числе i++ (инкремент) – «Пока условие истинно, делай вот это».
do...while
do…while отличается от while тем, что тело цикла выполнится хотя бы один раз, даже если условие false.
let i = 0;
do {
console.log(i); // Выведет 0
i++;
} while (i < 0); // Условие false, но цикл сработал 1 раз
Здесь заметно, что цикл начинается с ключевого слова do и заканчивается while. Это значит, что принцип такой – «Сделай вот это. Повторяй, пока условие истинно».
Отличия циклов
★ Таблица отличий циклов
| Цикл | Когда использовать | Особенности |
|---|---|---|
| for | Известно количество итераций | Компактный синтаксис |
| while | Неизвестно количество итераций | Может не выполниться ни разу |
| do…while | Проверка после итерации | Выполнится минимум 1 раз |
Операции выполняются в рамках функций, а циклы и условные операторы – части функций, словно инструменты. Каждая функция выполняется в соответствии с её телом. В общем случае JS работает в одном потоке, и долгие операции могут замораживать страницу, поэтому надо разбивать тяжёлые задачи на части. Однако, в ряде случаев, браузер «зависает» именно при ожидании результата выполнения функции. Тут мы подходим к асинхронности.
Цикл for...in
Цикл for...in перебирает перечисляемые свойства объекта. Он проходит по ключам объекта, а не по значениям.
const user = {
name: "Джон",
age: 30,
email: "john@example.com"
};
for (let key in user) {
console.log(key); // "name", "age", "email"
console.log(user[key]); // "Джон", 30, "john@example.com"
}
Цикл for...in работает с любыми объектами:
const car = {
brand: "Toyota",
model: "Camry",
year: 2020,
color: "blue"
};
for (let property in car) {
console.log(`${property}: ${car[property]}`);
}
// brand: Toyota
// model: Camry
// year: 2020
// color: blue
Цикл перебирает свойства в произвольном порядке. Свойства наследуемые через прототип также включаются в перебор:
const animal = {
eats: true
};
const rabbit = {
jumps: true,
__proto__: animal
};
for (let prop in rabbit) {
console.log(prop); // "jumps", "eats"
}
Для проверки принадлежности свойства самому объекту используется метод hasOwnProperty:
for (let prop in rabbit) {
if (rabbit.hasOwnProperty(prop)) {
console.log(`Собственное свойство: ${prop}`);
}
}
// Собственное свойство: jumps
Цикл for...of
Цикл for...of перебирает итерируемые объекты. Он проходит по значениям, а не по ключам.
const colors = ["red", "green", "blue"];
for (let color of colors) {
console.log(color); // "red", "green", "blue"
}
Цикл работает с массивами, строками и другими итерируемыми объектами:
const text = "JavaScript";
for (let char of text) {
console.log(char);
}
// J, a, v, a, S, c, r, i, p, t
Цикл for...of удобен для работы с коллекциями:
const numbers = [1, 2, 3, 4, 5];
let sum = 0;
for (let num of numbers) {
sum += num;
}
console.log(sum); // 15
Цикл поддерживает работу с множествами и картами:
const uniqueNumbers = new Set([1, 2, 3, 4, 5]);
for (let num of uniqueNumbers) {
console.log(num);
}
const phoneBook = new Map([
["Джон", "555-1234"],
["Мария", "555-5678"]
]);
for (let [name, phone] of phoneBook) {
console.log(`${name}: ${phone}`);
}
Цикл for await...of
Цикл for await...of работает с асинхронными итераторами. Он позволяет обрабатывать последовательность промисов.
async function processItems() {
const items = [
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve(3)
];
for await (let item of items) {
console.log(item); // 1, 2, 3
}
}
Цикл полезен для последовательной обработки асинхронных операций:
async function fetchUsers() {
const userIds = [1, 2, 3, 4, 5];
for await (let id of userIds) {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
console.log(user.name);
}
}
Цикл работает с асинхронными генераторами:
async function* asyncGenerator() {
yield Promise.resolve(1);
yield Promise.resolve(2);
yield Promise.resolve(3);
}
async function process() {
for await (let value of asyncGenerator()) {
console.log(value);
}
}
Управление потоком выполнения
break
Оператор break прекращает выполнение цикла. Выполнение программы продолжается со следующей инструкции после цикла.
for (let i = 0; i < 10; i++) {
if (i === 5) {
break;
}
console.log(i); // 0, 1, 2, 3, 4
}
Оператор работает во всех видах циклов:
let i = 0;
while (i < 10) {
if (i === 5) {
break;
}
console.log(i);
i++;
}
Метки позволяют выходить из вложенных циклов:
outerLoop: for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (i === 1 && j === 1) {
break outerLoop;
}
console.log(`i=${i}, j=${j}`);
}
}
continue
Оператор continue пропускает текущую итерацию цикла. Выполнение переходит к следующей итерации.
for (let i = 0; i < 10; i++) {
if (i % 2 === 0) {
continue;
}
console.log(i); // 1, 3, 5, 7, 9
}
Оператор работает с любыми циклами:
let i = 0;
while (i < 10) {
i++;
if (i % 3 === 0) {
continue;
}
console.log(i); // 1, 2, 4, 5, 7, 8, 10
}
Метки работают с continue аналогично break:
outer: for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (j === 1) {
continue outer;
}
console.log(`i=${i}, j=${j}`);
}
}
return
Оператор return завершает выполнение функции. Он возвращает значение из функции вызывающему коду.
function findValue(array, target) {
for (let item of array) {
if (item === target) {
return item;
}
}
return undefined;
}
const result = findValue([1, 2, 3, 4, 5], 3);
console.log(result); // 3
Оператор return прекращает выполнение функции немедленно:
function process(data) {
if (!data) {
return;
}
console.log("Обработка данных");
// Этот код не выполнится, если данные отсутствуют
}
В стрелочных функциях return может быть неявным:
const multiply = (a, b) => a * b;
const square = x => {
return x * x;
};
throw
Оператор throw создаёт исключение. Он прерывает нормальное выполнение программы.
function divide(a, b) {
if (b === 0) {
throw new Error("Деление на ноль невозможно");
}
return a / b;
}
try {
const result = divide(10, 0);
} catch (error) {
console.log(error.message); // "Деление на ноль невозможно"
}
Оператор работает с различными типами ошибок:
function validateAge(age) {
if (age < 0) {
throw new RangeError("Возраст не может быть отрицательным");
}
if (typeof age !== "number") {
throw new TypeError("Возраст должен быть числом");
}
return true;
}
Исключения можно создавать с собственными классами:
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
function validateEmail(email) {
if (!email.includes("@")) {
throw new ValidationError("Некорректный формат email");
}
return true;
}
Практические примеры
Поиск элемента в массиве:
function findUser(users, id) {
for (let user of users) {
if (user.id === id) {
return user;
}
}
throw new Error(`Пользователь с id ${id} не найден`);
}
Фильтрация данных:
function getActiveUsers(users) {
const result = [];
for (let user of users) {
if (!user.active) {
continue;
}
result.push(user);
}
return result;
}
Обработка вложенных структур:
function processMatrix(matrix) {
for (let i = 0; i < matrix.length; i++) {
for (let j = 0; j < matrix[i].length; j++) {
if (matrix[i][j] === null) {
break;
}
console.log(matrix[i][j]);
}
}
}
Асинхронная обработка:
async function processQueue(queue) {
for await (let task of queue) {
try {
await task.execute();
} catch (error) {
console.error("Ошибка выполнения задачи:", error);
continue;
}
}
}