Skip to content

Latest commit

 

History

History
497 lines (346 loc) · 31.6 KB

lesson17.md

File metadata and controls

497 lines (346 loc) · 31.6 KB

Урок 17. Циклы (for, while), функции: обычные / function expression / стрелочные

От разовых действий к повторяемой и переиспользуемой логике

В прошлом уроке мы научились принимать решения (if/тернарник) и работать с одиночными значениями. Но реальный код почти никогда не ограничивается одним значением и одним решением — нужно обработать список фильмов, посчитать цену билета для разных возрастов, повторить какое-то действие много раз. Для этого существуют циклы.

А чтобы не переписывать одну и ту же логику (например, расчёт скидки на билет из прошлого урока) заново каждый раз, когда она нужна — существуют функции, оборачивающие код в переиспользуемый, именованный блок. Это направление вам уже отлично знакомо по Python — сегодня разберём, как та же идея выглядит в JavaScript.


Цикл for — классический счётчик

for (let i = 0; i < 5; i++) {
  console.log(i);
}
// 0
// 1
// 2
// 3
// 4

Три части в скобках for (...), разделённые точкой с запятой:

  1. Инициализация (let i = 0) — выполняется один раз, перед первым проходом цикла.
  2. Условие (i < 5) — проверяется перед каждым проходом; как только становится false — цикл останавливается.
  3. Шаг (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 его запрещает.


Цикл while — пока условие истинно

let count = 0;

while (count < 5) {
  console.log(count);
  count++;
}
// 0, 1, 2, 3, 4

while проверяет условие перед каждым проходом — если условие сразу false, тело цикла не выполнится ни разу. Используется, когда заранее неизвестно точное количество повторений — например, пока пользователь не введёт корректное значение, пока не найден нужный элемент, пока не закончатся элементы очереди.

do...while — гарантированный первый проход

let attempts = 0;

do {
  console.log(`Попытка №${attempts + 1}`);
  attempts++;
} while (attempts < 3);

Отличие от обычного while: тело выполнится хотя бы один раз, даже если условие изначально ложно — проверка происходит после первого прохода, а не до него. На практике do...while используется значительно реже, чем for и while, но полезно знать его существование при чтении чужого кода.


break и continue — управление ходом цикла

// 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, но именно свойство, а не отдельная функция.

Перебор массива классическим for

const movies = ['Матрица', 'Начало', 'Интерстеллар'];

for (let i = 0; i < movies.length; i++) {
  console.log(`${i + 1}. ${movies[i]}`);
}
// 1. Матрица
// 2. Начало
// 3. Интерстеллар

for...of — современный и более читаемый способ перебора

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 declaration)

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='Гость'):).

Функция без return

function logMessage(text) {
  console.log(text);
  // нет return
}

const result = logMessage('Привет');
console.log(result); // undefined

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


Function expression — функция как значение переменной

const greetUser = function (name) {
  return `Привет, ${name}!`;
};

console.log(greetUser('Мария')); // Привет, Мария!

Здесь функция не объявляется отдельной конструкцией, а создаётся как значение, присвоенное переменной — точно так же, как мы присваивали переменной число или строку. Это важная идея, с которой в Python вы, возможно, ещё не сталкивались так явно: в JavaScript функция — это полноценное значение, которое можно положить в переменную, передать в другую функцию как аргумент, вернуть из функции как результат.

Ключевое практическое различие: hoisting (поднятие объявлений)

// 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.


Стрелочные функции (arrow functions)

Самый современный и часто используемый в реальном коде синтаксис объявления функций.

const greetUser = (name) => {
  return `Привет, ${name}!`;
};

console.log(greetUser('Мария')); // Привет, Мария!

Сокращённая запись: неявный return

Если тело функции состоит из одного выражения, фигурные скобки и 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 — это одна из причин, по которой они появились в языке. Мы разберём это подробно в уроке про классы, когда 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 вместо let — раскрываем подробнее в уроке про области видимости

// Работает "как ожидается" в большинстве простых случаев, но имеет скрытые проблемы
// при использовании счётчика внутри асинхронного кода или замыкания — разберём это позже
for (var i = 0; i < 5; i++) {
  console.log(i);
}

Пока просто держите в голове правило — используйте let для счётчика цикла, а не var. Подробное объяснение, почему это важно, будет в уроке про области видимости и замыкания.

Вызов function expression до её объявления

console.log(calculatePrice(400, 20)); // ReferenceError

const calculatePrice = (basePrice, age) => basePrice;

Разобрано в разделе 6 — function expression и стрелочные функции недоступны до строки, где им присвоено значение, в отличие от function declaration.

Забытый return, если функция должна что-то вернуть

// Забыли return — функция ничего не возвращает, хотя, судя по имени, должна была
const calculateDiscount = (price, percent) => {
  const discount = (price * percent) / 100;
  // забыли return discount;
};

const discount = calculateDiscount(400, 20);
console.log(discount); // undefined — а не 80, как ожидалось

Если функция должна вернуть значение, а не просто вывести его в консоль или выполнить побочное действие — не забывайте return, особенно при использовании полной формы стрелочной функции с фигурными скобками (в отличие от сокращённой формы с неявным возвратом, где return подразумевается автоматически).

Скобки для стрелочной функции с телом vs без тела

// Ошибка: тело в фигурных скобках требует явного 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), объекты и деструктуризация. Эти темы тесно связаны с сегодняшними циклами и функциями: многие методы массивов принимают функцию как аргумент — то есть используют именно то, что мы сегодня разобрали.


Вопросы для проверки

  1. В чём разница между for и while? Когда естественнее выбрать один, а когда — другой?
  2. Чем for...of отличается от классического for при переборе массива?**
  3. В чём разница между break и continue?
  4. Что такое hoisting и как оно объясняет, почему function declaration можно вызвать до её объявления в коде, а function expression и стрелочную функцию — нельзя?
  5. Что вернёт функция, если в её теле нет ключевого слова return?
  6. Почему в этом курсе рекомендуется использовать стрелочные функции как основной стиль для новых функций, но при этом важно уметь читать function declaration?
  7. Почему стрелочная функция с телом в фигурных скобках без явного return возвращает undefined, хотя её тело состоит из единственного выражения?

Практические задания

Задание 1. Пронумерованный список фильмов (циклы)

Имея массив (пока в минимальном виде, из раздела 4 урока):

const movies = ['Матрица', 'Начало', 'Интерстеллар', 'Довод', 'Дюна'];
  1. Выведите список в консоль в пронумерованном виде (1. Матрица, 2. Начало, ...), используя классический for.
  2. Выведите тот же список, используя for...of вместе с .entries().
  3. Дополнительно: используя for с break, найдите и выведите в консоль первый фильм, название которого длиннее 8 символов (используйте .length у строки — свойство длины строки работает точно так же, как и у массива).

Задание 2. Функция проверки возрастного рейтинга (рефакторинг Урока 16)

В Уроке 16 мы писали код определения возрастного рейтинга фильма (6+, 12+, 16+, 18+) как разовый скрипт. Превратите его в функцию:

  1. Функция getAgeRating(age), стрелочная, принимает возраст и возвращает строку рейтинга.
  2. Используйте её для группы зрителей: const visitorsAges = [4, 10, 14, 17, 25];
  3. В цикле выведите для каждого зрителя строку вида Возраст 4: рейтинг "Просмотр не рекомендован".

Задание 3. Прайс-лист кинотеатра для группы (объединяющая практика: функция + цикл + условия)

Используя функцию calculateTicketPrice из раздела 9 этого урока (расчёт цены с учётом возрастной скидки):

  1. Заведите массив имён и возрастов зрителей группы — можно в виде двух параллельных массивов: const names = ['Аня', 'Борис', 'Вера']; и const ages = [8, 45, 65]; (полноценную работу с массивом объектов, где имя и возраст хранятся вместе, разберём в следующем уроке).
  2. Базовая цена билета basePrice — 400 рублей.
  3. В цикле для каждого зрителя (используйте индекс, чтобы обратиться сразу к обоим массивам) выведите строку вида: Аня (8 лет): 200 руб. (скидка 50%).
  4. Дополнительно посчитайте и выведите общую сумму всех билетов группы.

Предыдущий урок | Следующий урок