К списку уроков

Урок 4. JavaScript: переменные, типы, операторы, условия, циклы и функции

Онлайн и в Б-3, 8 октября (четверг), 15:00

Предисловие

Для вашего удобства текстовый курс перенесен на отдельную платформу. Ссылка для поступления: тык

Мы настоятельно рекомендуем пользоваться именно этой платформой по ходу курса. Ссылка на текущее занятие: тык

⚠️Прежде чем переходить к заданиям - обязательно зарегистрируйтесь на курс. Иначе вас закинет на общий поток и вы не сможете взаимодействовать с сверстниками во втором модуле курса. Это не смертельно и вполне решаемо, но тем не менее.

JavaScript: переменные, типы, операторы, условия, циклы и функции

Подключение JavaScript

JavaScript — язык программирования, который выполняет браузер. На нём пишут поведение страницы: реакцию на нажатия, проверку форм, загрузку данных с сервера. Программа на JavaScript состоит из инструкций, которые браузер выполняет по очереди, сверху вниз.

Код хранят в отдельном файле с расширением .js и подключают к странице тегом <script>:

<!DOCTYPE html>
<html lang="ru">
  <head>
    <meta charset="UTF-8" />
    <title>Каталог</title>
    <script defer src="script.js"></script>
  </head>
  <body>
    <h1>Каталог</h1>
  </body>
</html>

Атрибут defer откладывает выполнение скрипта, пока браузер не построит всю страницу. Без defer скрипт в <head> выполнится раньше, чем появится <body>, и не найдёт на странице ни одного элемента. Второй способ добиться того же результата — поставить <script src="script.js"></script> последним тегом внутри <body>.

Первое время мы будем работать не со страницей, а с консолью. Команда console.log выводит значения в консоль инструментов разработчика:

console.log('Привет, консоль!');
console.log(2 + 2);
console.log('Цена:', 89990);

Нажмите «Показать результат», чтобы выполнить пример. У примеров JavaScript в курсе вместо страницы открывается консоль.

console.log принимает любое количество значений через запятую и выводит их через пробел. Когда непонятно, что происходит в программе, выведите нужные значения в консоль и посмотрите на них.

Почитать ещё

Инструкции и комментарии

Программа состоит из инструкций. Каждая инструкция — одно действие: объявить переменную, вызвать функцию, сравнить значения. Инструкции обычно записывают по одной на строку.

console.log('Первая инструкция');
console.log('Вторая инструкция');
console.log('Третья'); console.log('и четвёртая на одной строке');

В конце инструкции ставят точку с запятой. Если её пропустить, JavaScript в большинстве случаев сам определит конец инструкции по переносу строки. Но иногда он склеивает две строки в одну инструкцию, и код ломается в неожиданном месте:

const price = 100
[1, 2, 3].forEach((n) => console.log(n))

JavaScript прочитает эти строки как const price = 100[1, 2, 3].forEach(...) и выдаст ошибку Cannot read properties of undefined. Поэтому в этом курсе и в проекте ITAM Board точку с запятой ставят всегда. В рабочих проектах за этим следят линтер ESLint и форматировщик Prettier: они подсвечивают пропущенные точки с запятой и расставляют их сами.

Комментарии — текст в коде, который JavaScript не выполняет. Комментарии объясняют неочевидные решения и временно отключают часть кода.

// Однострочный комментарий: действует до конца строки

/*
  Многострочный комментарий:
  действует до закрывающих символов
*/

console.log('Эта строка выполнится');
// console.log('А эта нет')

В VS Code строку можно закомментировать и раскомментировать сочетанием Ctrl+/ (на Mac — Cmd+/).

JavaScript различает заглавные и строчные буквы. console.log работает, а Console.Log вызовет ошибку.

Почитать ещё

Переменные: let и const

Переменная — именованное место, где хранится значение. Переменную объявляют словом let или const, затем пишут имя и после знака = начальное значение.

let count = 0;
count = count + 1;
console.log(count);

const price = 89990;
console.log(price);
price = 79990;

Разница между словами такая:

  • переменной, объявленной через let, можно присвоить новое значение;

  • переменной, объявленной через const, новое значение присвоить нельзя. Попытка сделать это вызывает ошибку, которую вы увидите в предпросмотре выше.

По умолчанию переменные объявляют через const, а let используют, только когда значение действительно будет меняться, например у счётчика. Так читатель кода сразу видит, какие значения меняются в программе.

В старом коде встречается третье слово — var. У него запутанные правила видимости, поэтому в новом коде его не используют.

Правила для имён переменных:

  • имя состоит из букв, цифр, символов $ и _ и не может начинаться с цифры;

  • нельзя использовать зарезервированные слова языка: let, const, if, return и другие;

  • имена из нескольких слов пишут в стиле camelCase: первое слово со строчной буквы, каждое следующее с заглавной: productTitle, isInStock, totalPrice.

Имя переменной должно объяснять, что в ней хранится. Имя price понятнее, чем p, а inStockProducts понятнее, чем arr2.

Почитать ещё

Типы данных

Каждое значение в JavaScript относится к какому-то типу. От типа зависит, что можно делать со значением: числа можно умножать, у строк можно узнать длину.

const title = 'Ноутбук';
const price = 89990;
const rating = 4.7;
const inStock = true;
const discount = null;
let delivery;

console.log(typeof title);
console.log(typeof price);
console.log(typeof rating);
console.log(typeof inStock);
console.log(typeof delivery);

Тип

Значения

Пример

string

Строки, то есть текст

'Ноутбук'

number

Числа, целые и дробные

89990, 4.7

boolean

Логические значения

true, false

null

Специальное значение «ничего нет»

null

undefined

Специальное значение «значение не задано»

переменная без начального значения

object

Объекты и массивы: наборы значений

{ title: 'Ноутбук' }, [1, 2, 3]

Оператор typeof возвращает название типа значения в виде строки.

null и undefined означают отсутствие значения, но используются в разных ситуациях. undefined получается само: так выглядит переменная без начального значения или несуществующее свойство объекта. null программист записывает сам, когда хочет явно показать, что значения нет, например у товара нет скидки.

У typeof null есть исторически сложившаяся ошибка: оператор возвращает 'object', хотя null — отдельный тип.

Почитать ещё

Строки

Строку записывают в одинарных '...', двойных "..." или обратных кавычках .... Одинарные и двойные кавычки работают одинаково. Строка в обратных кавычках называется шаблонной: в неё можно подставить значение через ${...}, и в ней можно переносить строки.

const title = 'Ноутбук';
const price = 89990;

console.log(`${title} стоит ${price} ₽`);
console.log(`Со скидкой 10%: ${price * 0.9} ₽`);

Строка — последовательность символов. У каждого символа есть номер, который называется индексом. Нумерация начинается с нуля.

const title = 'Ноутбук Laptop 14';

console.log(title.length);
console.log(title[0]);
console.log(title.at(-1));
console.log(title.toUpperCase());
console.log(title.toLowerCase());
console.log(title.includes('Laptop'));
console.log(title.slice(0, 7));
console.log('  лишние пробелы  '.trim());
console.log(title.replaceAll(' ', '-'));
console.log(title.split(' '));
  • length — длина строки. Это свойство, поэтому скобки после него не ставят.

  • title[0] — символ с индексом 0. at(-1) возвращает последний символ: отрицательный индекс считается с конца.

  • toUpperCase() и toLowerCase() переводят строку в верхний и нижний регистр.

  • includes(подстрока) проверяет, есть ли подстрока в строке, и возвращает true или false.

  • slice(начало, конец) возвращает часть строки от индекса начало до индекса конец, не включая его.

  • trim() убирает пробелы в начале и в конце строки.

  • replaceAll(что, чем) заменяет все вхождения подстроки.

  • split(разделитель) разбивает строку на массив частей.

Методы строк не меняют исходную строку, а возвращают новую. После title.toUpperCase() в переменной title остаётся прежнее значение.

Почитать ещё

Числа и арифметика

В JavaScript один тип для всех чисел — number. Дробную часть отделяют точкой: 4.7.

console.log(10 + 3);
console.log(10 - 3);
console.log(10 * 3);
console.log(10 / 3);
console.log(10 % 3);
console.log(2 ** 10);
  • % — остаток от деления. 10 % 3 равно 1, потому что 10 = 3 × 3 + 1. Остатком удобно проверять делимость: число n делится на 3, если n % 3 === 0.

  • ** — возведение в степень.

Сокращённые операторы меняют значение переменной:

let total = 100;
total += 50;
console.log(total);
total -= 30;
console.log(total);
total *= 2;
console.log(total);
total++;
console.log(total);

total += 50 — короткая запись для total = total + 50. total++ увеличивает значение на единицу, total-- уменьшает.

Для округления и других вычислений есть встроенный объект Math:

console.log(Math.round(4.5));
console.log(Math.floor(4.7));
console.log(Math.ceil(4.2));
console.log(Math.max(12, 5, 30));
console.log(Math.min(12, 5, 30));
console.log((4.567).toFixed(1));
console.log(0.1 + 0.2);
  • Math.round округляет до ближайшего целого, Math.floor — вниз, Math.ceil — вверх.

  • Math.max и Math.min возвращают наибольшее и наименьшее из переданных чисел.

  • Метод toFixed(1) округляет число до одного знака после точки и возвращает строку.

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

Если математическая операция не может дать число, результатом будет специальное значение NaN (Not a Number): например, 'абв' * 2. Любая операция с NaN тоже даёт NaN.

Почитать ещё

Преобразование типов

Иногда значение одного типа нужно превратить в значение другого. Например, всё, что пользователь вводит в поле формы, приходит строкой, даже цифры. Прежде чем складывать такие значения, их превращают в числа.

console.log(Number('42'));
console.log(Number(''));
console.log(Number('42 рубля'));
console.log(parseInt('42px'));
console.log(String(42));
console.log(Boolean(''));
console.log(Boolean('текст'));
  • Number(значение) превращает значение в число. Если строка не является числом, результат — NaN. Пустая строка превращается в 0.

  • parseInt(строка) читает целое число с начала строки и останавливается на первом символе, который не является цифрой.

  • String(значение) превращает значение в строку.

  • Boolean(значение) превращает значение в true или false. Какие значения превращаются в false, разберём в разделе про условия.

JavaScript умеет преобразовывать типы и сам, когда в операции участвуют значения разных типов. Такое неявное преобразование часто даёт неожиданный результат:

console.log('5' + 2);
console.log('5' - 2);
console.log('5' * '2');
console.log('Товаров: ' + 3 + 2);

Оператор + со строкой склеивает значения, поэтому '5' + 2 даёт строку '52'. Операторы - и * работают только с числами, поэтому превращают строки в числа. В последней строке сначала выполняется 'Товаров: ' + 3, получается строка, и к ней приклеивается 2.

Чтобы не зависеть от неявных правил, преобразуйте типы явно: Number(input) + 2.

Почитать ещё

Сравнение

Операторы сравнения возвращают true или false.

const price = 89990;

console.log(price > 50000);
console.log(price <= 50000);
console.log(price === 89990);
console.log(price !== 89990);
console.log('89990' === 89990);
console.log('89990' == 89990);
  • >, <, >=, <= — больше, меньше, больше или равно, меньше или равно.

  • === — строгое равенство. Значения равны, только если совпадают и тип, и значение. Строка '89990' не равна числу 89990.

  • !== — строгое неравенство.

  • == и != — нестрогое равенство и неравенство. Перед сравнением они преобразуют значения к одному типу, поэтому '89990' == 89990 даёт true.

Правила преобразования при нестрогом сравнении запутанные: например, '' == 0 тоже даёт true. Поэтому в коде используют только === и !==.

Строки сравниваются посимвольно, по порядку символов в таблице Unicode: 'a' < 'b' даёт true. Заглавные латинские буквы в этой таблице стоят раньше строчных, поэтому 'Z' < 'a' тоже даёт true.

Почитать ещё

Логические операторы

Логические операторы объединяют условия.

const inStock = true;
const price = 8990;

console.log(inStock && price < 10000);
console.log(!inStock || price < 5000);
console.log(!inStock);
  • && (И) возвращает true, если оба условия истинны.

  • || (ИЛИ) возвращает true, если истинно хотя бы одно из условий.

  • ! (НЕ) меняет значение на противоположное.

Операторы && и || возвращают не обязательно true или false, а одно из исходных значений. || возвращает первое истинное значение, а если такого нет, то последнее. На этом построен приём со значением по умолчанию:

const userName = '';
console.log(userName || 'Гость');

const quantity = 0;
console.log(quantity || 1);
console.log(quantity ?? 1);

У приёма с || есть проблема: он подставляет запасное значение вместо любого ложного, включая 0 и пустую строку. Если количество товара равно нулю, quantity || 1 всё равно вернёт 1.

Оператор ?? подставляет запасное значение, только если слева null или undefined. Для чисел и строк, у которых ноль и пустая строка — допустимые значения, используйте ??.

Почитать ещё

Условия: if и else

Инструкция if выполняет код, только если условие истинно.

const price = 32990;

if (price > 100000) {
  console.log('Дорогой товар');
} else if (price > 10000) {
  console.log('Товар средней цены');
} else {
  console.log('Недорогой товар');
}

Условие записывают в круглых скобках, а код, который нужно выполнить, в фигурных. Ветку else if проверяют, только если предыдущие условия ложны. Ветка else выполняется, если не подошло ни одно условие. Обе ветки необязательны.

В условии может стоять не только true или false, но любое значение. JavaScript превращает его в логическое. Значения, которые превращаются в false, называют ложными:

  • false;

  • 0;

  • '' — пустая строка;

  • null;

  • undefined;

  • NaN.

Все остальные значения истинные, включая строку '0', пустой массив [] и пустой объект {}.

const cart = [];
const promoCode = '';

if (promoCode) {
  console.log('Промокод применён');
} else {
  console.log('Промокод не введён');
}

if (cart) {
  console.log('Пустой массив — истинное значение');
}

if (cart.length === 0) {
  console.log('Корзина пуста');
}

Чтобы проверить, пуст ли массив, сравнивайте его длину, а не сам массив.

Почитать ещё

Тернарный оператор и switch

Если в зависимости от условия нужно выбрать одно из двух значений, удобен тернарный оператор:

const inStock = false;
const label = inStock ? 'В наличии' : 'Нет в наличии';
console.log(label);

Запись читается так: «условие ? значение, если условие истинно : значение, если ложно». Тернарные операторы не вкладывают друг в друга: такой код трудно читать, и лучше записать его через if.

Инструкция switch сравнивает одно значение с несколькими вариантами:

const category = 'audio';
let categoryName;

switch (category) {
  case 'laptops':
    categoryName = 'Ноутбуки';
    break;
  case 'audio':
    categoryName = 'Аудиотехника';
    break;
  default:
    categoryName = 'Другое';
}

console.log(categoryName);

switch сравнивает значение с каждым case через строгое равенство. Когда вариант совпал, выполняется код до ближайшего break. Если break забыть, выполнение продолжится в следующем case. Ветка default выполняется, если ни один вариант не подошёл.

Часто вместо switch используют объект, в котором ключи — варианты, а значения — результат. Объекты разберём чуть ниже, а пока посмотрите, насколько короче получается запись:

const categoryNames = { laptops: 'Ноутбуки', audio: 'Аудиотехника' };
const category = 'audio';

console.log(categoryNames[category] ?? 'Другое');

Почитать ещё

Массивы

Массив хранит упорядоченный список значений. Массив записывают в квадратных скобках, элементы разделяют запятыми.

const categories = ['laptops', 'audio', 'monitors'];

console.log(categories[0]);
console.log(categories.length);
console.log(categories.at(-1));
console.log(categories[5]);

Как и у строк, у элементов массива есть индексы, которые начинаются с нуля. Последний элемент имеет индекс length - 1, а метод at(-1) возвращает его короче. Обращение к несуществующему индексу даёт undefined.

У массивов много методов. Вот самые частые:

const categories = ['laptops', 'audio'];

categories.push('monitors');
console.log(categories);

const last = categories.pop();
console.log(last, categories);

console.log(categories.includes('audio'));
console.log(categories.indexOf('audio'));
console.log(categories.indexOf('cables'));
console.log(categories.join(', '));
console.log([10, 20, 30, 40].slice(1, 3));
  • push(элемент) добавляет элемент в конец массива.

  • pop() удаляет последний элемент и возвращает его.

  • includes(значение) проверяет, есть ли значение в массиве.

  • indexOf(значение) возвращает индекс значения или -1, если значения нет.

  • join(разделитель) склеивает элементы в строку.

  • slice(начало, конец) возвращает новый массив из части элементов.

Методы push и pop меняют исходный массив, а slice и join возвращают новое значение и исходный массив не трогают. Это различие станет важным во втором модуле курса.

Массив объявлен через const, но push работает. const запрещает присвоить переменной другой массив, но не запрещает менять содержимое текущего.

Элементом массива может быть другой массив. Так хранят таблицы: table[1][0] означает «первый элемент второй строки».

const table = [
  [1, 2, 3],
  [4, 5, 6],
];

console.log(table[1][0]);
console.log(table[0].length);

Почитать ещё

Объекты

Объект хранит набор свойств. Каждое свойство — пара «ключ: значение». Объект записывают в фигурных скобках.

const product = {
  id: 1,
  title: 'Laptop 14',
  price: 89990,
  inStock: true,
  specs: {
    ram: 16,
    screen: 14,
  },
};

console.log(product.title);
console.log(product['price']);
console.log(product.specs.ram);
console.log(product.color);

Прочитать свойство можно двумя способами: через точку product.title или через квадратные скобки product['title']. Квадратные скобки нужны, когда имя свойства хранится в переменной:

const product = { title: 'Laptop 14', price: 89990 };
const field = 'price';

console.log(product[field]);

product.price = 79990;
product.discount = 10;
delete product.discount;
console.log(product);
  • Присваивание существующему свойству меняет его значение.

  • Присваивание несуществующему свойству добавляет его.

  • Оператор delete удаляет свойство.

  • Чтение несуществующего свойства возвращает undefined и ошибкой не считается.

Ошибка возникает, если читать свойство у undefined или null. Например, если у товара нет вложенного объекта specs, запись product.specs.ram остановит программу с ошибкой. Оператор опциональной цепочки ?. возвращает undefined вместо ошибки:

const product = { title: 'Кабель USB-C' };

console.log(product.specs?.ram);
console.log(product.specs.ram);

Функции Object.keys и Object.values возвращают массив ключей и массив значений объекта:

const stock = { laptops: 2, audio: 5, monitors: 0 };

console.log(Object.keys(stock));
console.log(Object.values(stock));

Почитать ещё

Ссылки на объекты и копирование

Строки, числа и логические значения копируются целиком. Объекты и массивы копируются по ссылке: переменная хранит не сам объект, а ссылку на него.

let a = 100;
let b = a;
b = 200;
console.log(a);

const first = { price: 100 };
const second = first;
second.price = 200;
console.log(first.price);

После const second = first обе переменные указывают на один и тот же объект. Изменение через second видно через first, потому что объект один.

Сравнение объектов тоже работает по ссылкам. Два объекта с одинаковыми свойствами не равны, если это разные объекты:

const first = { price: 100 };
const second = { price: 100 };
const same = first;

console.log(first === second);
console.log(first === same);

Чтобы получить копию объекта или массива, используют оператор ..., о котором подробнее в следующем разделе:

const product = { title: 'Laptop 14', price: 89990 };
const copy = { ...product };
copy.price = 79990;

console.log(product.price);
console.log(copy.price);

Такая копия называется поверхностной: копируются только свойства верхнего уровня. Если в свойстве лежит вложенный объект, копия и оригинал будут ссылаться на один и тот же вложенный объект.

Почитать ещё

Деструктуризация и spread

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

const product = { title: 'Laptop 14', price: 89990, category: 'laptops' };
const { title, price } = product;
console.log(title, price);

const sizes = ['S', 'M', 'L'];
const [smallest, medium] = sizes;
console.log(smallest, medium);

При деструктуризации объекта имена переменных совпадают с ключами. При деструктуризации массива значения берутся по порядку.

Деструктуризацию можно записать прямо в параметрах функции. Функции разберём дальше, а пока обратите внимание на запись в скобках:

function printProduct({ title, price }) {
  console.log(`${title}: ${price} ₽`);
}

printProduct({ title: 'Laptop 14', price: 89990 });

Spread — оператор ..., который раскладывает элементы массива или свойства объекта внутри нового массива или объекта.

const categories = ['laptops', 'audio'];
const allCategories = [...categories, 'monitors'];
console.log(allCategories);

const product = { title: 'Laptop 14', price: 89990 };
const discounted = { ...product, price: 79990 };
console.log(discounted);
console.log(product);

В записи { ...product, price: 79990 } сначала копируются все свойства product, а затем price получает новое значение. Исходный объект не меняется. Этот приём понадобится во втором модуле: в React данные не меняют, а создают их изменённые копии.

Почитать ещё

Циклы

Цикл повторяет код несколько раз. Классический цикл for считает повторения с помощью переменной-счётчика:

for (let i = 1; i <= 3; i++) {
  console.log(`Повтор номер ${i}`);
}

В скобках три части через точку с запятой: начальное значение счётчика, условие продолжения и действие после каждого повтора. Цикл выполняется, пока условие истинно.

Чтобы пройти по массиву, счётчик используют как индекс. Если индекс не нужен, короче записать цикл for...of, который по очереди даёт каждый элемент:

const prices = [89990, 12990, 6490];

for (let i = 0; i < prices.length; i++) {
  console.log(`Товар ${i}: ${prices[i]} ₽`);
}

let total = 0;
for (const price of prices) {
  total += price;
}
console.log(total);

Цикл while повторяет код, пока условие истинно. Его используют, когда число повторов заранее неизвестно:

let budget = 30000;
let bought = 0;

while (budget >= 12990) {
  budget -= 12990;
  bought++;
}

console.log(bought, budget);

Внутри любого цикла работают две команды. break сразу завершает цикл, continue пропускает остаток текущего повтора и переходит к следующему.

const prices = [6490, 0, 12990, 149990, 2490];

for (const price of prices) {
  if (price === 0) {
    continue;
  }
  if (price > 100000) {
    console.log('Нашли дорогой товар, дальше не ищем');
    break;
  }
  console.log(price);
}

Циклы часто используют для подсчёта. Объект подходит, чтобы считать, сколько раз встретилось каждое значение:

const categories = ['audio', 'laptops', 'audio', 'monitors', 'audio'];
const counts = {};

for (const category of categories) {
  counts[category] = (counts[category] ?? 0) + 1;
}

console.log(counts);

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

Почитать ещё

Функции

Функция — фрагмент кода с именем, который можно выполнить много раз с разными данными.

function formatPrice(price) {
  return `${price.toLocaleString('ru-RU')} ₽`;
}

console.log(formatPrice(89990));
console.log(formatPrice(1200));
  • formatPrice — имя функции. Имя функции обычно начинается с глагола: formatPrice, getTotal, isValid.

  • price в скобках — параметр. При вызове formatPrice(89990) параметр получает значение 89990, которое называют аргументом.

  • return завершает функцию и возвращает результат. Если функция закончилась без return, она возвращает undefined.

Параметров может быть несколько. У параметра может быть значение по умолчанию, которое используется, если аргумент не передали:

function getDiscountPrice(price, percent = 10) {
  if (percent <= 0) {
    return price;
  }
  return price - (price * percent) / 100;
}

console.log(getDiscountPrice(1000, 25));
console.log(getDiscountPrice(1000));
console.log(getDiscountPrice(1000, 0));

В этом примере первый return завершает функцию раньше, если скидки нет. Такой приём избавляет от лишнего else.

Переменные, объявленные внутри функции, существуют только внутри неё. Это правило называется областью видимости. Функция видит переменные, объявленные снаружи, но код снаружи не видит переменные функции. То же правило действует для любого блока в фигурных скобках: переменная, объявленная через let или const внутри if или цикла, не видна за его пределами.

const currency = '₽';

function formatTotal(total) {
  const rounded = Math.round(total);
  return `${rounded} ${currency}`;
}

console.log(formatTotal(1234.56));
console.log(rounded);

Функция formatTotal использует внешнюю переменную currency, и это работает. Последняя строка примера вызывает ошибку: переменная rounded объявлена внутри функции и снаружи не существует.

Почитать ещё

Стрелочные функции и колбэки

Функцию можно записать короче, в виде стрелочной функции:

const formatPrice = (price) => `${price.toLocaleString('ru-RU')} ₽`;

const getTotal = (prices) => {
  let total = 0;
  for (const price of prices) {
    total += price;
  }
  return total;
};

console.log(formatPrice(89990));
console.log(getTotal([100, 200, 300]));
  • Если тело стрелочной функции — одно выражение, фигурные скобки и return не пишут: значение выражения возвращается само.

  • Если в теле несколько инструкций, их пишут в фигурных скобках, и return обязателен.

  • Чтобы вернуть объект из короткой записи, его оборачивают в круглые скобки: (title) => ({ title }). Без скобок JavaScript примет фигурные скобки объекта за тело функции.

Функция в JavaScript — такое же значение, как число или строка. Её можно сохранить в переменную и передать в другую функцию. Функция, которую передали другой функции, чтобы та её вызвала, называется колбэком.

function repeat(times, action) {
  for (let i = 1; i <= times; i++) {
    action(i);
  }
}

repeat(3, (step) => {
  console.log(`Шаг ${step}`);
});

Функция repeat не знает, что именно нужно сделать. Она вызывает переданный колбэк нужное число раз, а что делает колбэк, решает тот, кто вызывает repeat. На колбэках построены методы массивов из следующих разделов, обработчики событий и работа с сетью.

Почитать ещё

Function Declaration и Function Expression

В предыдущих разделах функции записывались двумя способами.

Первый способ называется Function Declaration, или объявление функции. Инструкция начинается со слова function, за которым идёт имя функции:

function getTotal(prices) {
  let total = 0;
  for (const price of prices) {
    total += price;
  }
  return total;
}

Второй способ называется Function Expression, или функциональное выражение. Функцию создают внутри выражения, а результат сохраняют в переменную. Так можно записать и обычную функцию, и стрелочную:

const getTotal = function (prices) {
  let total = 0;
  for (const price of prices) {
    total += price;
  }
  return total;
};

const formatPrice = (price) => `${price.toLocaleString('ru-RU')} ₽`;

Колбэк из прошлого раздела тоже является функциональным выражением: функцию создают прямо в месте вызова и сразу передают аргументом.

Функции, созданные обоими способами, вызывают одинаково: getTotal([100, 200]). Главное отличие в том, с какого момента функцию можно вызвать.

Объявление можно вызвать раньше

Перед выполнением кода JavaScript находит все объявления через function и создаёт эти функции заранее. Поэтому функцию, объявленную через Function Declaration, можно вызвать в коде выше её объявления. Такое поведение называют поднятием (hoisting).

console.log(formatPrice(89990));

function formatPrice(price) {
  return `${price.toLocaleString('ru-RU')} ₽`;
}

Функциональное выражение создаёт функцию, только когда JavaScript доходит до его строки. До этого момента в переменной formatPrice ещё нет значения, и вызов заканчивается ошибкой:

console.log(formatPrice(89990));

const formatPrice = (price) => `${price.toLocaleString('ru-RU')} ₽`;

Консоль покажет ошибку Cannot access 'formatPrice' before initialization. Если перенести вызов ниже объявления переменной, код заработает.

Выражение можно записать там, где нужно значение

Объявление через function всегда стоит отдельной инструкцией. Функциональное выражение можно написать везде, где JavaScript ожидает значение: сохранить в переменную, передать аргументом или выбрать с помощью условия.

const isWholesale = true;

const calcPrice = isWholesale
  ? (price) => price * 0.8
  : (price) => price;

console.log(calcPrice(1000));

В этом примере в переменную calcPrice попадает одна из двух функций. Какая именно, зависит от значения isWholesale. С помощью Function Declaration так записать нельзя.

Какой способ выбрать

Оба способа встречаются в реальных проектах, и выбор обычно зависит от договорённостей команды. В проекте ITAM Board функции модулей и React-компоненты объявляют через function, а короткие функции и колбэки записывают стрелочными функциями:

function getCardTitles(cards) {
  return cards.map((card) => card.title);
}

Такой код удобно читать сверху вниз: крупные функции видны по слову function, а небольшие колбэки не отвлекают от главного.

Почитать ещё

Методы массивов: forEach, map, filter, find

У массивов есть методы, которые принимают колбэк и вызывают его для каждого элемента. Они заменяют большинство циклов.

const products = [
  { title: 'Laptop 14', price: 89990, inStock: true },
  { title: 'Наушники Studio', price: 12490, inStock: false },
  { title: 'Мышь Click', price: 2490, inStock: true },
];

products.forEach((product) => {
  console.log(product.title);
});

const titles = products.map((product) => product.title);
console.log(titles);

const available = products.filter((product) => product.inStock);
console.log(available.length);

const cheap = products.find((product) => product.price < 5000);
console.log(cheap);

console.log(products.some((product) => product.price > 50000));
console.log(products.every((product) => product.inStock));

Метод

Что возвращает

forEach

Ничего. Просто вызывает колбэк для каждого элемента.

map

Новый массив той же длины из значений, которые вернул колбэк.

filter

Новый массив из элементов, для которых колбэк вернул истинное значение.

find

Первый элемент, для которого колбэк вернул истинное значение, или undefined.

findIndex

Индекс такого элемента или -1.

some

true, если колбэк вернул истинное значение хотя бы для одного элемента.

every

true, если колбэк вернул истинное значение для всех элементов.

Колбэк получает текущий элемент первым аргументом и его индекс вторым: products.map((product, index) => ...).

Методы map и filter не меняют исходный массив. Их можно выстроить в цепочку, где каждый следующий метод работает с результатом предыдущего:

const products = [
  { title: 'Laptop 14', price: 89990 },
  { title: 'Наушники Studio', price: 12490 },
  { title: 'Мышь Click', price: 2490 },
];

const affordableTitles = products
  .filter((product) => product.price < 20000)
  .map((product) => product.title.toUpperCase());

console.log(affordableTitles);

Цепочка читается как описание задачи: «оставить товары дешевле 20 000, взять их названия заглавными буквами». Тот же результат можно получить циклом, но он будет длиннее.

Частая ошибка с map — колбэк в фигурных скобках без return. Такой колбэк возвращает undefined, и массив результата состоит из undefined.

Почитать ещё

Методы массивов: reduce и сортировка

Метод reduce сворачивает массив в одно значение: сумму, максимум, объект с подсчётами.

const prices = [89990, 12490, 2490];

const total = prices.reduce((sum, price) => sum + price, 0);
console.log(total);

const max = prices.reduce((best, price) => (price > best ? price : best), prices[0]);
console.log(max);

reduce принимает два аргумента: колбэк и начальное значение. Колбэк получает накопленное значение и текущий элемент и возвращает новое накопленное значение. В первом примере накопленное значение начинается с 0, и на каждом шаге к нему прибавляется цена. Результат последнего вызова колбэка и есть результат reduce.

Метод sort сортирует массив. У него есть две особенности:

const numbers = [10, 9, 1, 25];
numbers.sort();
console.log(numbers);

const prices = [10, 9, 1, 25];
const ascending = prices.toSorted((a, b) => a - b);
console.log(ascending);
console.log(prices);
  • Без колбэка sort сравнивает элементы как строки, поэтому 10 оказывается раньше 9: строка '10' начинается с символа '1'.

  • sort меняет исходный массив.

Чтобы сортировать числа, передают функцию сравнения. Она получает два элемента a и b и возвращает отрицательное число, если a должен стоять раньше, положительное, если позже, и ноль, если порядок не важен. Запись (a, b) => a - b сортирует по возрастанию, (a, b) => b - a — по убыванию.

Метод toSorted сортирует так же, но возвращает новый массив и не трогает исходный. Если исходный массив менять нельзя, используйте toSorted или сортируйте копию: [...prices].sort((a, b) => a - b).

Массив объектов сортируют по нужному свойству:

const products = [
  { title: 'Laptop 14', price: 89990 },
  { title: 'Мышь Click', price: 2490 },
  { title: 'Наушники Studio', price: 12490 },
];

const byPrice = products.toSorted((a, b) => a.price - b.price);
console.log(byPrice.map((product) => product.title));

Почитать ещё

  • Дока: .reduce(), .sort()

  • MDN: Array.prototype.toSorted()

Задания