Конвейер: преобразовывать данные вместо того, чтобы ими командовать
Почти вся работа фронтендера — одна форма: с сервера приехал список объектов, и он должен стать чем-то на экране. Научись описывать это преобразование конвейером — и бо́льшая часть кода перестанет быть бухгалтерией.
Возьмём одну маленькую задачу дважды. Есть список заказов, нужна сумма оплаченных.
Написанное циклом: заводишь накопитель, ставишь счётчик, проверяешь границу, увеличиваешь, обращаешься по индексу, проверяешь условие, прибавляешь к накопителю и в конце возвращаешь. Восемь решений, из которых одно про заказы и семь про то, как правильно крутить цикл.
Написанное конвейером: оставить оплаченные, взять суммы, сложить. Три шага, и все три про заказы.
Работают оба. Разница в том, что ты сможешь прочитать через год, и в том, сколько мест есть у опечатки, чтобы спрятаться.
Цикл говорит как. Конвейер говорит что. Второе читается и через год — а именно тогда это и важно.
Форма фронтендных данных
Почти всё, с чем ты будешь иметь дело, имеет одну форму: массив объектов. Список товаров, сообщений, пользователей, строк — приезжает 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 и опиши преобразование словами до написания кода: «оставить активные, взять имя и сумму, отсортировать по сумме убыванию». Эта фраза и есть конвейер; дальше её остаётся набрать.
Проверь себя
Закрой статью и ответь своими словами:
- Какая одна форма данных покрывает бо́льшую часть фронтендной работы?
- Что возвращают
map,filter,find,someиreduce? - Назови три мутирующих метода и три возвращающих новое.
- Почему сортировка массива может сломать экран, которого ты не трогал?
- Что такое поверхностная копия и когда её перестаёт хватать?
- В чём разница между
||и??и какой баг она вызывает? - Почему тренировать выбор метода — не то же самое, что тренировать метод?
Коротко
- Фронтендные данные почти всегда массив объектов, и работа состоит в описании его преобразования, а не в администрировании цикла.
- Выбирай метод по тому, что он возвращает:
map— та же длина,filter— короче,find— один,some/every— булево,reduce— свёртка. sortмутирует и по умолчанию сравнивает как строки — передавай компаратор или бериtoSorted.- Деление на мутирующие и возвращающие новое — источник невидимых багов, особенно вместе со ссылками.
- Spread копирует поверхностно; вложенным данным нужна более глубокая копия, а фреймворки определяют изменение по ссылке, поэтому мутация молча пропускает перерисовку.
- Деструктуризация, spread/rest,
?.и??убирают бо́льшую часть шума из реального кода, а??означает «отсутствует», а не «ложно». - Функции — значения; предпочитай те, что возвращают, а не меняют, называй их по результату и возвращайся рано.
- Тренируй выбор метода, вперемешку и с разносом по дням, пока частые преобразования не станут одной мыслью.