Учебник веб-разработки
Разделы учебника
На этой странице

II–III. JS, TypeScript и модели данных

JavaScript: значения, функции и ссылки

Оглавление

Задача: превратить список событий в список подписей и понять, что изменяется при выполнении программы. Нужны множества и отображения; синтаксис вводим здесь. Все данные ниже учебные. Сохраните первый пример как lesson.mjs в своей учебной ветке и запустите node lesson.mjs из каталога файла.

Выражение и последовательность действий

Выражение вычисляет значение: 2 + 3 даёт число, "Привет" — строку, count > 0 — логическое значение. Инструкция задаёт действие: объявляет имя, выбирает ветку или возвращает результат функции.

const events = [
  { title: "Открытие", location: "Площадь" },
  { title: "Мастерская", location: "Шатёр" },
];

function label(event) {
  return `${event.title} — ${event.location}`;
}

for (const event of events) {
  console.log(label(event));
}

events — имя массива, { ... } создаёт объект с полями, event.title читает поле. function объявляет функцию; параметр получает значение аргумента при вызове. return завершает вызов и отдаёт результат. for...of перебирает элементы массива. Шаблонная строка в обратных кавычках вставляет значения выражений ${...}. Здесь console.log — наблюдаемый эффект: запись в консоль.

Имя, объявленное через const, нельзя связать с другим значением. let позволяет присваивание: let count = 0; count = count + 1. Объявления внутри блока { ... } видны в этом блоке и вложенных блоках. Предпочитайте const, пока переприсваивание не нужно.

Значения и операции

В JavaScript есть примитивы: undefined, null, boolean, number, bigint, string, symbol — и объекты. Массивы и функции также объекты; функцию можно передать другой функции. Это классификация языка, а не типы нашего предметного мира. Спецификация ECMAScript.

undefined часто означает отсутствие значения свойства или результата, null — явно выбранное отсутствие. Их смысл в API нужно договорить. typeof null исторически возвращает "object"; одной этой проверки недостаточно для работы с объектом. Для массивов есть Array.isArray(value).

console.log(0.1 + 0.2 === 0.3); // false
console.log("2" + 3); // "23"
console.log(2 + 3); // 5
console.log(null === undefined); // false

number не моделирует точные вещественные числа: округление меняет привычные алгебраические законы. === не выполняет преобразование строк в числа; оператор + может складывать числа или соединять строки. Поэтому внешний ввод сначала разбирают, а не полагаются на неявное преобразование.

Для ветвления используйте if (условие) { ... } else { ... }. В условии значения преобразуются к boolean: пустая строка, 0, null, undefined ложны, а пустой массив и пустой объект истинны. value ?? fallback выбирает запасное значение только для null/undefined; value || fallback — для любого ложного значения. Если ноль допустим, подмена его запасным значением через || может быть ошибкой.

Выбор варианта и короткие выражения

switch сравнивает значение с вариантами case. Здесь каждая ветка завершает функцию через return, поэтому выполнение не переходит в следующий case:

function statusLabel(status) {
  switch (status) {
    case "draft":
      return "Черновик";
    case "published":
      return "Опубликовано";
    default:
      return "Неизвестный статус";
  }
}
console.log(statusLabel("published")); // Опубликовано

default обрабатывает остальные значения. Без return или break выполнение может продолжиться в соседней ветке. Проверка типов never в ADT — дополнительный слой TypeScript, а не свойство обычного JavaScript switch.

condition ? yes : no выбирает одно из двух выражений. Destructuring извлекает поля или элементы: const { title, location } = events[0], const [first] = events. Это короткая форма чтения, она не копирует вложенные объекты.

Два имени могут указывать на один объект

const original = { title: "Открытие", venue: { name: "Площадь" } };
const alias = original;
alias.title = "Начало";
console.log(original.title); // "Начало"
console.log(alias === original); // true

const copy = { ...original };
copy.title = "Другой заголовок";
copy.venue.name = "Шатёр";
console.log(original.title); // "Начало"
console.log(original.venue.name); // "Шатёр"

Присваивание объекта копирует ссылку, не весь граф данных. Spread { ...original } создаёт новый внешний объект, но вложенный venue остаётся общим. const запрещает переприсваивание имени, а не изменение полей. Два отдельно созданных объекта с одинаковыми полями не равны через ===. Объекты и их сравнение в MDN.

Исходник схемы
flowchart LR
  Original["original"] --> Object["Объект события"]
  Alias["alias"] --> Object
  Copy["copy"] --> New["Новый внешний объект"]
  Object --> Venue["Общий объект venue"]
  New --> Venue

Стрелка означает ссылку. Копирование внешнего объекта не разрывает последнюю общую ссылку. Для изменения названия площадки без изменения исходника создайте и новый вложенный объект: { ...original, venue: { ...original.venue, name: "Шатёр" } }.

Преобразование массива

map применяет функцию к каждому элементу и собирает результаты. filter оставляет элементы, для которых предикат истинен. find возвращает первый подходящий элемент или undefined. Методы Array.

const onSquare = events.filter((event) => event.location === "Площадь");
const titles = onSquare.map((event) => event.title);
console.log(titles); // ["Открытие"]
console.log(events.find((event) => event.title === "Неизвестное")); // undefined

(event) => event.title — стрелочная функция; это сокращённый способ записать вычисление результата. Callback получает элемент массива. Новые внешние массивы не означают новые объекты элементов: onSquare[0] === events[0] здесь истинно. sort меняет массив, на котором вызван; [...events].sort(compare) сортирует копию внешнего массива. map тоже может менять вход, если callback мутирует объект.

Сводный итог и коллекция ключей

reduce последовательно передаёт накопитель следующему вызову callback. Начальное значение делает поведение пустого массива явным:

const totalLetters = events.reduce((sum, event) => sum + event.title.length, 0);
const byLocation = new Map();
for (const event of events) {
  const previous = byLocation.get(event.location) ?? 0;
  byLocation.set(event.location, previous + 1);
}
console.log(totalLetters, [...byLocation]);

Map хранит пары ключ/значение: get читает, set записывает. Это отдельная коллекция, а map массива преобразует каждый элемент. ...byLocation перебирает пары в порядке добавления ключей. Локальная запись в новую коллекцию не меняет исходный массив; это правило пригодится в FP-главе.

Замыкание и модуль

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

function atLocation(location) {
  return (event) => event.location === location;
}
const isOnSquare = atLocation("Площадь");
console.log(events.filter(isOnSquare).length); // 1

location остаётся доступен после возврата из atLocation. Замыкание может также захватить изменяемое значение, поэтому само наличие замыкания не означает чистую функцию.

Модуль отделяет публичные имена от внутренних. В Node файл .mjs — ESM-модуль:

// labels.mjs
export function label(event) {
  return `${event.title} — ${event.location}`;
}
// lesson.mjs, рядом с labels.mjs
import { label } from "./labels.mjs";
console.log(label({ title: "Открытие", location: "Площадь" }));

В браузере нет всего API Node, в Node нет браузерного DOM. Язык общий, окружение предоставляет разные возможности. Правила модулей и сборки проекта разбираются вместе с инструментами.

Где это видно в Atmanki

Группировка расписания использует цикл, массивы, Map и локальные объекты. Она сортирует копию входа и создаёт структуру дней/площадок. Сам Map — коллекция пар ключ/значение, не метод массива map. Файл написан на TypeScript; аннотации разберём в следующей главе.

Практика и самопроверка

Напишите titlesAt(events, location) на учебных данных первого примера. Функция возвращает массив заголовков нужной площадки и не меняет вход. Проверьте обычный случай, неизвестную площадку и пустой массив. Затем измените элемент результата: исходный заголовок должен сохраниться, ведь результат содержит строки, а не объекты событий.

Ожидается: titlesAt(events, "Площадь") → ["Открытие"], titlesAt(events, "Нет") → []. Объясните, почему копия массива событий не позволяла бы так же безопасно менять поля его элементов.