В прошлом уроке мы научились принимать решения (if/тернарник) и работать с одиночными значениями. Но реальный код почти никогда не ограничивается одним значением и одним решением — нужно обработать список фильмов, посчитать цену билета для разных возрастов, повторить какое-то действие много раз. Для этого существуют циклы.
А чтобы не переписывать одну и ту же логику (например, расчёт скидки на билет из прошлого урока) заново каждый раз, когда она нужна — существуют функции, оборачивающие код в переиспользуемый, именованный блок. Это направление вам уже отлично знакомо по Python — сегодня разберём, как та же идея выглядит в JavaScript.
for (let i = 0; i < 5; i++) {
console.log(i);
}
// 0
// 1
// 2
// 3
// 4Три части в скобках for (...), разделённые точкой с запятой:
- Инициализация (
let i = 0) — выполняется один раз, перед первым проходом цикла. - Условие (
i < 5) — проверяется перед каждым проходом; как только становитсяfalse— цикл останавливается. - Шаг (
i++) — выполняется после каждого прохода, перед следующей проверкой условия.
Сравнение с Python: в Python вы обычно пишете for i in range(5): — цикл сам управляет счётчиком через range(). В JavaScript счётчик приходится задавать явно, всеми тремя частями — это более "низкоуровневый" синтаксис, зато он даёт полный контроль: можно менять шаг счётчика, идти в обратном порядке, использовать сложные условия остановки.
// Обратный отсчёт
for (let i = 5; i > 0; i--) {
console.log(i);
}
// 5, 4, 3, 2, 1
// Шаг больше единицы
for (let i = 0; i <= 10; i += 2) {
console.log(i);
}
// 0, 2, 4, 6, 8, 10Важно про let в цикле: счётчик цикла всегда объявляется через let, никогда через const — очевидная причина в том, что i++ внутри тела цикла — это переприсваивание, а const его запрещает.
let count = 0;
while (count < 5) {
console.log(count);
count++;
}
// 0, 1, 2, 3, 4while проверяет условие перед каждым проходом — если условие сразу false, тело цикла не выполнится ни разу. Используется, когда заранее неизвестно точное количество повторений — например, пока пользователь не введёт корректное значение, пока не найден нужный элемент, пока не закончатся элементы очереди.
let attempts = 0;
do {
console.log(`Попытка №${attempts + 1}`);
attempts++;
} while (attempts < 3);Отличие от обычного while: тело выполнится хотя бы один раз, даже если условие изначально ложно — проверка происходит после первого прохода, а не до него. На практике do...while используется значительно реже, чем for и while, но полезно знать его существование при чтении чужого кода.
// break — немедленно прерывает цикл целиком
for (let i = 0; i < 10; i++) {
if (i === 5) {
break;
}
console.log(i);
}
// 0, 1, 2, 3, 4 — на i === 5 цикл полностью останавливается
// continue — пропускает текущий проход, переходит к следующему
for (let i = 0; i < 5; i++) {
if (i === 2) {
continue;
}
console.log(i);
}
// 0, 1, 3, 4 — итерация с i === 2 пропущена, но цикл продолжился дальшеОба ключевых слова работают точно так же, как и в Python — синтаксис здесь полностью идентичен.
Прежде чем перейти к for...of, нам нужен минимальный словарь про массивы — полноценно мы разберём их в следующем уроке, а сегодня возьмём только то, что нужно для перебора.
const movies = ['Матрица', 'Начало', 'Интерстеллар'];
console.log(movies[0]); // 'Матрица' — обращение по индексу, отсчёт с 0, как и в Python
console.log(movies.length); // 3 — количество элементов
console.log(movies[movies.length - 1]); // 'Интерстеллар' — последний элементМассив в JavaScript — прямой аналог списка (list) в Python: упорядоченная коллекция значений, доступ по индексу с отсчётом от нуля, свойство .length — аналог len(...) в Python, но именно свойство, а не отдельная функция.
const movies = ['Матрица', 'Начало', 'Интерстеллар'];
for (let i = 0; i < movies.length; i++) {
console.log(`${i + 1}. ${movies[i]}`);
}
// 1. Матрица
// 2. Начало
// 3. Интерстелларconst movies = ['Матрица', 'Начало', 'Интерстеллар'];
for (const movie of movies) {
console.log(movie);
}
// Матрица
// Начало
// Интерстелларfor...of перебирает сами значения массива напрямую, без явного счётчика и обращения по индексу — это прямой аналог for movie in movies: из Python. Если индекс всё же нужен (например, для нумерации, как в примере с классическим for выше), используйте entries():
const movies = ['Матрица', 'Начало', 'Интерстеллар'];
for (const [index, movie] of movies.entries()) {
console.log(`${index + 1}. ${movie}`);
}Забегая вперёд: есть ещё
for...in— но он предназначен для перебора объектов (структур ключ-значение, аналог словарей Python), а не массивов, и на массивах его использование считается плохой практикой (есть тонкости с порядком и типом получаемых значений). Подробно разберём его в следующем уроке, когда дойдём до объектов.
Практическое правило этого курса: для перебора массивов используйте for...of как основной инструмент — он читается яснее классического for там, где сам индекс не нужен. Классический for со счётчиком оставляйте для случаев, где действительно важен явный контроль над числом (нумерация, шаг больше единицы, обратный порядок).
function greetUser(name) {
return `Привет, ${name}!`;
}
console.log(greetUser('Мария')); // Привет, Мария!Структура: function + имя + параметры в скобках + тело в фигурных скобках + return для возврата значения. Это прямой аналог def greetUser(name): в Python, с тем же смыслом return.
function greetUser(name = 'Гость') {
return `Привет, ${name}!`;
}
console.log(greetUser()); // Привет, Гость!
console.log(greetUser('Мария')); // Привет, Мария!Прямой аналог значений по умолчанию у параметров функций Python (def greetUser(name='Гость'):).
function logMessage(text) {
console.log(text);
// нет return
}
const result = logMessage('Привет');
console.log(result); // undefinedЕсли функция не содержит return, она всё равно что-то возвращает вызывающему коду — а именно undefined. Это стоит помнить: отсутствие return не значит "функция ничего не возвращает" в смысле ошибки — это осознанное поведение языка, возвращающее конкретное значение undefined.
const greetUser = function (name) {
return `Привет, ${name}!`;
};
console.log(greetUser('Мария')); // Привет, Мария!Здесь функция не объявляется отдельной конструкцией, а создаётся как значение, присвоенное переменной — точно так же, как мы присваивали переменной число или строку. Это важная идея, с которой в Python вы, возможно, ещё не сталкивались так явно: в JavaScript функция — это полноценное значение, которое можно положить в переменную, передать в другую функцию как аргумент, вернуть из функции как результат.
// Function declaration — можно вызвать ДО объявления в коде
console.log(sayHi()); // Привет! — сработает без ошибки
function sayHi() {
return 'Привет!';
}// Function expression — нельзя вызвать до объявления
console.log(sayBye()); // ReferenceError: Cannot access 'sayBye' before initialization
const sayBye = function () {
return 'Пока!';
};Function declaration (function name() {}) "поднимается" интерпретатором JavaScript в начало текущей области видимости ещё до выполнения кода — поэтому её можно вызвать выше по коду, чем она написана. Function expression (const name = function() {}) ведёт себя как обычная переменная — она доступна только после той строки, где ей присвоено значение.
На практике это означает: не полагайтесь на порядок объявления функций как на что-то само собой разумеющееся — определяйте функцию до её первого использования, независимо от того, какой синтаксис вы выбрали. Это делает код понятнее для чтения сверху вниз, вне зависимости от технических тонкостей hoisting.
Самый современный и часто используемый в реальном коде синтаксис объявления функций.
const greetUser = (name) => {
return `Привет, ${name}!`;
};
console.log(greetUser('Мария')); // Привет, Мария!Если тело функции состоит из одного выражения, фигурные скобки и return можно опустить — значение выражения возвращается автоматически:
const greetUser = (name) => `Привет, ${name}!`;
console.log(greetUser('Мария')); // Привет, Мария!const double = (x) => x * 2;
const double2 = x => x * 2; // скобки вокруг одного параметра необязательны
console.log(double(5)); // 10
console.log(double2(5)); // 10Практическая рекомендация этого курса: для единообразия и читаемости всегда пишите скобки вокруг параметров, даже если параметр один ((x) => x * 2, а не x => x * 2) — так синтаксис остаётся одинаковым независимо от числа параметров, и меньше риск ошибиться при добавлении второго параметра в будущем.
const sayHi = () => 'Привет!';
console.log(sayHi()); // Привет!Пустые скобки обязательны, если параметров нет — их нельзя опустить (в отличие от случая с одним параметром).
Стрелочные функции ведут себя иначе, чем обычные, в контексте ключевого слова this — это одна из причин, по которой они появились в языке. Мы разберём это подробно в уроке про классы, когда this станет практически значимым понятием — сегодня просто держим в уме, что синтаксическая компактность — не единственная причина использовать стрелочные функции, есть и более глубокая техническая причина, к которой мы ещё вернёмся.
| Синтаксис | Пример | Когда использовать |
|---|---|---|
| Function declaration | function foo() {} |
Основные, самостоятельные функции верхнего уровня, особенно если порядок вызова относительно объявления не важен |
| Function expression | const foo = function() {} |
Реже используется сама по себе; полезна, когда явно хочется подчеркнуть, что функция — это значение |
| Arrow function | const foo = () => {} |
Короткие функции, особенно передаваемые как аргумент в другую функцию (что мы увидим в следующем уроке с методами массивов); стандарт в современном коде |
Практическая рекомендация этого курса: используйте стрелочные функции как основной инструмент для новых, самостоятельно написанных функций — это самый распространённый стиль в современных проектах. Function declaration стоит знать и уметь читать — вы будете часто встречать её в чужом коде и документации. Function expression — скорее промежуточное звено для понимания, как устроены два других варианта, чем то, что вы будете часто писать сами.
В Уроке 16 мы написали скрипт, вычисляющий цену билета со скидкой — но это была разовая логика, "зашитая" прямо в код, без возможности переиспользования. Превратим её в функцию.
const calculateTicketPrice = (basePrice, age) => {
let discountPercent;
if (age < 12) {
discountPercent = 50;
} else if (age < 18) {
discountPercent = 20;
} else if (age >= 60) {
discountPercent = 30;
} else {
discountPercent = 0;
}
const finalPrice = basePrice - (basePrice * discountPercent) / 100;
return { finalPrice, discountPercent };
};
const result = calculateTicketPrice(400, 15);
console.log(`Цена: ${result.finalPrice} руб., скидка: ${result.discountPercent}%`);
// Цена: 320 руб., скидка: 20%Что изменилось и почему это лучше: теперь логику расчёта можно вызвать для любого возраста и любой базовой цены сколько угодно раз, не копируя код заново. Функция возвращает объект с двумя значениями сразу (finalPrice и discountPercent) — предвосхищение темы следующего урока, но интуитивно понятное уже сейчас: это просто способ вернуть из функции больше одного значения одновременно, похоже на возврат кортежа в Python (return finalPrice, discountPercent).
const calculateTicketPrice = (basePrice, age) => {
let discountPercent;
if (age < 12) {
discountPercent = 50;
} else if (age < 18) {
discountPercent = 20;
} else if (age >= 60) {
discountPercent = 30;
} else {
discountPercent = 0;
}
return basePrice - (basePrice * discountPercent) / 100;
};
const groupAges = [8, 15, 25, 45, 65];
const basePrice = 400;
for (const age of groupAges) {
const price = calculateTicketPrice(basePrice, age);
console.log(`Возраст ${age}: билет за ${price} руб.`);
}
// Возраст 8: билет за 200 руб.
// Возраст 15: билет за 320 руб.
// Возраст 25: билет за 400 руб.
// Возраст 45: билет за 400 руб.
// Возраст 65: билет за 280 руб.Здесь хорошо видна сила связки "функция + цикл": вся логика расчёта описана один раз внутри функции, а for...of просто применяет её к каждому значению массива — ни одной повторяющейся строчки логики скидок.
// Опасно: i никогда не меняется, условие i < 5 всегда истинно — цикл никогда не остановится
let i = 0;
while (i < 5) {
console.log(i);
}Браузер (или Node.js) в таком случае просто "зависнет", выполняя цикл бесконечно. Всегда проверяйте, что внутри тела while действительно есть шаг, изменяющий переменную, участвующую в условии.
// Работает "как ожидается" в большинстве простых случаев, но имеет скрытые проблемы
// при использовании счётчика внутри асинхронного кода или замыкания — разберём это позже
for (var i = 0; i < 5; i++) {
console.log(i);
}Пока просто держите в голове правило — используйте let для счётчика цикла, а не var. Подробное объяснение, почему это важно, будет в уроке про области видимости и замыкания.
console.log(calculatePrice(400, 20)); // ReferenceError
const calculatePrice = (basePrice, age) => basePrice;Разобрано в разделе 6 — function expression и стрелочные функции недоступны до строки, где им присвоено значение, в отличие от function declaration.
// Забыли return — функция ничего не возвращает, хотя, судя по имени, должна была
const calculateDiscount = (price, percent) => {
const discount = (price * percent) / 100;
// забыли return discount;
};
const discount = calculateDiscount(400, 20);
console.log(discount); // undefined — а не 80, как ожидалосьЕсли функция должна вернуть значение, а не просто вывести его в консоль или выполнить побочное действие — не забывайте return, особенно при использовании полной формы стрелочной функции с фигурными скобками (в отличие от сокращённой формы с неявным возвратом, где return подразумевается автоматически).
// Ошибка: тело в фигурных скобках требует явного return
const double = (x) => { x * 2 };
console.log(double(5)); // undefined — фигурные скобки восприняты как блок кода, а не как объект,
// и без return функция ничего не вернула
// Правильно — либо явный return внутри блока...
const double1 = (x) => { return x * 2 };
// ...либо сокращённая форма без фигурных скобок вообще
const double2 = (x) => x * 2;Частая путаница у новичков: если после стрелки стоят фигурные скобки, они воспринимаются как блок кода (как в обычной функции, с обязательным return), а не автоматически как возвращаемое значение.
Циклы for и while решают задачу повторения кода: for удобен, когда известно точное число повторений или нужен явный счётчик, while — когда повторение зависит от условия, а не от заранее известного количества шагов. break прерывает цикл целиком, continue пропускает только текущий проход. Для перебора массивов в современном коде предпочтителен for...of — он читается яснее, чем классический for со счётчиком, если сам индекс не нужен.
Функции упаковывают повторяющуюся логику в переиспользуемый блок — ровно так же, как def в Python, с той же ролью return. В JavaScript есть три способа объявить функцию: function declaration (function foo() {}, доступна до объявления в коде благодаря hoisting), function expression (const foo = function() {}, ведёт себя как обычная переменная) и стрелочная функция (const foo = () => {}, самый компактный и часто используемый в современном коде синтаксис, с сокращённой записью для однострочного тела и особым поведением this, которое мы разберём в уроке про классы).
В следующем уроке — массивы и их методы (map, filter, forEach, find), объекты и деструктуризация. Эти темы тесно связаны с сегодняшними циклами и функциями: многие методы массивов принимают функцию как аргумент — то есть используют именно то, что мы сегодня разобрали.
- В чём разница между
forиwhile? Когда естественнее выбрать один, а когда — другой? - Чем
for...ofотличается от классическогоforпри переборе массива?** - В чём разница между
breakиcontinue? - Что такое hoisting и как оно объясняет, почему function declaration можно вызвать до её объявления в коде, а function expression и стрелочную функцию — нельзя?
- Что вернёт функция, если в её теле нет ключевого слова
return? - Почему в этом курсе рекомендуется использовать стрелочные функции как основной стиль для новых функций, но при этом важно уметь читать function declaration?
- Почему стрелочная функция с телом в фигурных скобках без явного
returnвозвращаетundefined, хотя её тело состоит из единственного выражения?
Имея массив (пока в минимальном виде, из раздела 4 урока):
const movies = ['Матрица', 'Начало', 'Интерстеллар', 'Довод', 'Дюна'];- Выведите список в консоль в пронумерованном виде (
1. Матрица,2. Начало, ...), используя классическийfor. - Выведите тот же список, используя
for...ofвместе с.entries(). - Дополнительно: используя
forсbreak, найдите и выведите в консоль первый фильм, название которого длиннее 8 символов (используйте.lengthу строки — свойство длины строки работает точно так же, как и у массива).
В Уроке 16 мы писали код определения возрастного рейтинга фильма (6+, 12+, 16+, 18+) как разовый скрипт. Превратите его в функцию:
- Функция
getAgeRating(age), стрелочная, принимает возраст и возвращает строку рейтинга. - Используйте её для группы зрителей:
const visitorsAges = [4, 10, 14, 17, 25]; - В цикле выведите для каждого зрителя строку вида
Возраст 4: рейтинг "Просмотр не рекомендован".
Используя функцию calculateTicketPrice из раздела 9 этого урока (расчёт цены с учётом возрастной скидки):
- Заведите массив имён и возрастов зрителей группы — можно в виде двух параллельных массивов:
const names = ['Аня', 'Борис', 'Вера'];иconst ages = [8, 45, 65];(полноценную работу с массивом объектов, где имя и возраст хранятся вместе, разберём в следующем уроке). - Базовая цена билета
basePrice— 400 рублей. - В цикле для каждого зрителя (используйте индекс, чтобы обратиться сразу к обоим массивам) выведите строку вида:
Аня (8 лет): 200 руб. (скидка 50%). - Дополнительно посчитайте и выведите общую сумму всех билетов группы.