EverProduct
Фронтенд

Этап 03 · Язык

Конвейер: преобразовывать данные вместо того, чтобы ими командовать

Почти вся работа фронтендера — одна форма: с сервера приехал список объектов, и он должен стать чем-то на экране. Научись описывать это преобразование конвейером — и бо́льшая часть кода перестанет быть бухгалтерией.

Возьмём одну маленькую задачу дважды. Есть список заказов, нужна сумма оплаченных.

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

Написанное конвейером: оставить оплаченные, взять суммы, сложить. Три шага, и все три про заказы.

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

Цикл говорит как. Конвейер говорит что. Второе читается и через год — а именно тогда это и важно.

Форма фронтендных данных

Почти всё, с чем ты будешь иметь дело, имеет одну форму: массив объектов. Список товаров, сообщений, пользователей, строк — приезжает JSON-ом с сервера, фильтруется, сортируется, группируется и рисуется.

Значит, навык — это не «знать JavaScript». Это беглость в дюжине операций над массивами объектов плюс понимание, какая из них нужна в этой ситуации. Такая беглость стоит больше любого фреймворка, который ляжет сверху, и, в отличие от фреймворка, не протухает.

Дюжина операций

Каждая берёт функцию и применяет её к элементам. Учи, что каждая возвращает, — именно это решает, какая тебе нужна:

  • map — та же длина, каждый элемент преобразован. Объекты в имена, цены в отформатированные строки, данные в элементы на экране. Самый используемый метод во фронтенде, с большим отрывом.
  • filter — массив короче, остались прошедшие проверку.
  • find — сам первый подошедший элемент или undefined. Когда нужна вещь, а не список с ней внутри.
  • some / every — булево: прошёл ли хоть один / прошли ли все.
  • reduce — всё свёрнутое в одно значение: сумма, максимум, объект, сгруппированный по ключу. Мощно и легко пишется так, что никто не прочитает. Если reduce требует комментария, циклом было бы понятнее.
  • sort — упорядоченное. Две ловушки, и обе тебя укусят: он меняет исходный массив, а по умолчанию сравнивает элементы как строки, поэтому [10, 9, 1] превращается в [1, 10, 9]. Для чисел всегда передавай функцию сравнения.
  • slice — кусок, не трогая оригинал. Не путать со splice, который режет оригинал.
  • includes — есть ли там такое значение.
  • flat / flatMap — распрямить вложенность и сделать map с распрямлением за один проход.
  • Object.keys / values / entries — превратить объект в массивы, чтобы к нему применились методы выше.

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

Мутация: разделение, порождающее настоящие баги

Два класса методов, и путаница между ними даёт по-настоящему трудноуловимые баги.

Меняют оригинал: push, pop, shift, unshift, splice, sort, reverse и присваивание свойству.

Возвращают новое: map, filter, slice, concat, flat и более новые toSorted, toReversed и with, появившиеся именно потому, что мутирующие версии были ошибкой проектирования.

Соедини это с правилом ссылок из предыдущей статьи — и получишь классическую катастрофу: ты «копируешь» массив присваиванием, сортируешь копию, а оригинал тоже отсортирован, потому что копии никогда не было. Где-то список перерисовался в неверном порядке, и ничто в стеке вызовов не указывает на сортировку.

Копируют правильно через spread — одинаково для массивов и объектов, — и у этого есть предел, который надо знать: spread делает поверхностную копию. Верхний уровень новый; вложенные объекты внутри по-прежнему общие. Для по-настоящему вложенных данных копируй каждый изменяемый уровень или бери structuredClone.

Есть и специфически фронтендная причина этим озаботиться, помимо опрятности. Фреймворки решают, что перерисовывать, сравнивая ссылки: если массив — тот же самый объект, что и раньше, считается, что ничего не изменилось, как бы ни отличалось содержимое. Помутировать свои данные и недоумевать, почему экран не обновился, — обряд посвящения, и причина в этом абзаце. Через две статьи это будет правилом; здесь это механизм.

Синтаксис, убирающий шум

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

Деструктуризация вытаскивает поля из объекта или массива в именованные переменные одной строкой, с умолчаниями для отсутствующего. Именно так в современном коде обычно принимают параметры функций.

Spread и rest — одни и те же три точки в двух работах. Spread распаковывает массив или объект в другой — так делают копии и слияния. Rest собирает остаток — так функция принимает заранее неизвестное число аргументов.

Опциональная цепочкаuser?.profile?.city — возвращает undefined вместо падения, когда по пути чего-то нет. Она заменила цепочки из &&, которыми раньше был набит код обработки ответов API.

Оператор ?? подставляет запасное значение, только если исходное — null или undefined. Это не то же самое, что ||, и разница даёт настоящий баг: count || 10 заменяет законный 0 десяткой, а "" — твоей заглушкой. || — для «ложного», ?? — для «отсутствующего», и на практике ты почти всегда имеешь в виду отсутствующее.

Функции — это значения

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

Два свойства делают функцию приятной. Она возвращает результат, а не меняет что-то снаружи себя, и зависит только от аргументов, а не от окружающего состояния. Такая функция проверяется в изоляции, переиспользуется без сюрпризов и читается, не требуя знаний обо всём остальном, — и именно такую форму должна иметь бо́льшая часть твоей логики. А побочные эффекты — запись в DOM, обращение к серверу, изменение общего состояния — держи в нескольких обозначенных местах, а не рассыпай повсюду.

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

Тренируй выбор, а не применение

Это ровно та ситуация, которую описывает статья про связку ключей в сфере «Как учиться». Двадцать упражнений подряд на reduce тренируют применять reduce, а в настоящей работе задачу никто не подписывает, и настоящий навык — снять с кольца нужный ключ.

Поэтому тренируй выбор отдельно от решения. Возьми десять реалистичных задач — «три самых дорогих позиции», «есть ли хоть один неоплаченный счёт», «сколько заказов по каждому городу», «тот же список, отсортированный по дате» — и для каждой, не написав ни строчки, просто назови метод и почему. Пять минут, а тренируется тот самый ход, который учебники пропускают.

Ещё две вещи здесь окупаются. Разноси повторы: пройди тот же набор преобразований заново через три дня с пустого файла, а не продолжай в тот же день, потому что блок строит именно усилие вспоминания. И собирай чанк намеренно: через несколько недель «сгруппировать список по полю и отсортировать каждую группу» должно быть одной мыслью, а не пятнадцатиминутным исследованием. Это сращивание и есть вся разница между «читать JavaScript» и «писать на нём».

На практике

Перепиши три собственных цикла конвейерами. Потом прочитай обе версии вслух. Сравнение учит больше, чем статья.

Проговаривай тип возврата до выбора метода. «Мне нужен один элемент» — find. «Мне нужен список покороче» — filter. «Мне нужна та же длина, но преобразованная» — map. Большинство неверных выборов снимается этим одним вопросом.

Запрети себе мутирующие методы на неделю. toSorted вместо sort, spread вместо push. Ты почувствуешь, где мутация была несущей, и перестанешь удивляться общим ссылкам.

Цепочка в три шага — и стоп. Более длинный конвейер обычно просит промежуточную переменную с именем, объясняющим, что в ней лежит.

Прочитай настоящий ответ API и опиши преобразование словами до написания кода: «оставить активные, взять имя и сумму, отсортировать по сумме убыванию». Эта фраза и есть конвейер; дальше её остаётся набрать.

Проверь себя

Закрой статью и ответь своими словами:

  1. Какая одна форма данных покрывает бо́льшую часть фронтендной работы?
  2. Что возвращают map, filter, find, some и reduce?
  3. Назови три мутирующих метода и три возвращающих новое.
  4. Почему сортировка массива может сломать экран, которого ты не трогал?
  5. Что такое поверхностная копия и когда её перестаёт хватать?
  6. В чём разница между || и ?? и какой баг она вызывает?
  7. Почему тренировать выбор метода — не то же самое, что тренировать метод?

Коротко

  • Фронтендные данные почти всегда массив объектов, и работа состоит в описании его преобразования, а не в администрировании цикла.
  • Выбирай метод по тому, что он возвращает: map — та же длина, filter — короче, find — один, some/every — булево, reduce — свёртка.
  • sort мутирует и по умолчанию сравнивает как строки — передавай компаратор или бери toSorted.
  • Деление на мутирующие и возвращающие новое — источник невидимых багов, особенно вместе со ссылками.
  • Spread копирует поверхностно; вложенным данным нужна более глубокая копия, а фреймворки определяют изменение по ссылке, поэтому мутация молча пропускает перерисовку.
  • Деструктуризация, spread/rest, ?. и ?? убирают бо́льшую часть шума из реального кода, а ?? означает «отсутствует», а не «ложно».
  • Функции — значения; предпочитай те, что возвращают, а не меняют, называй их по результату и возвращайся рано.
  • Тренируй выбор метода, вперемешку и с разносом по дням, пока частые преобразования не станут одной мыслью.