Почему я больше не цепляю всё подряд в JavaScript — Мэтт Смит

Перевод статьи: Why I don't chain everything in JavaScript anymore - Matt Smith

Почему я больше не цепляю всё подряд в JavaScript — Мэтт Смит

Я раньше писал много JavaScript вот так:

const result = users
  .filter(user => user.active)
  .map(user => user.name)
  .sort()
  .slice(0, 5);

Ничего здесь не было неправильного. Я писал такой код постоянно. Но это как раз тот самый тип вещей, который сначала кажется нормальным, а потом постепенно становится всё труднее с ним работать.

Цепочки — это здорово… пока не перестают быть

Проблема не в .map() или .filter(). Проблема в том, что происходит, когда вы их складываете друг на друга. Вы перестаёте писать отдельные шаги и начинаете писать конвейеры.

Конвейеры выглядят аккуратно, но всё равно нужно мысленно пройтись по ним: filter → map → sort → slice.

Это нормально один-два раза. Делайте это по всему файлу — и начинает утомлять.

Сравните с этим:

const activeUsers = users.filter(user => user.active);
const names = activeUsers.map(user => user.name);

names.sort();

return names.slice(0, 5);

Да, строк больше. Но каждый шаг просто лежит перед глазами. Никакого «дешифрования» не требуется.

Та же проблема, но тремя способами

Вот то же намерение, записанное тремя разными способами.

Если я делаю цепочку:

users.filter(u => u.active).map(u => u.name)[0]

Это выглядит аккуратно. Раньше я часто тянулся к такому. Но оно обрабатывает всё, хотя мне нужен только один результат.

Если я пишу по шагам:

const user = users.find(u => u.active);
const name = user?.name;

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

Если мне нужен полный контроль:

for (const u of users) {
  if (u.active) return u.name;
}

Это самое явное — и, честно говоря, иногда самое понятное, когда мне действительно важно, что именно происходит.

Где всё начинает немного путаться

Это быстрее всего проявляется, когда вы пытаетесь отлаживать.

Допустим, что-то кажется не так, и вы хотите проверить отфильтрованные результаты. При цепочке вы в итоге делаете так:

const result = users
  .filter(user => {
    console.log(user);
    return user.active;
  })
  .map(user => user.name);

Теперь ваша логика перемешана с кодом отладки. Или вы сдаётесь и всё равно разрываете цепочку на части.

В итоге вы можете делать больше работы, чем нужно

Цепочки подталкивают к «обработай всё», даже когда вы на самом деле этого не имели в виду.

const firstActiveUser = users
  .filter(user => user.active)
  .map(user => user.name)[0];

Сначала фильтруется весь массив, затем результат мапится, а потом берётся один элемент.

А то, что вы на самом деле хотели:

const user = users.find(user => user.active);
const name = user?.name;

Или:

for (const user of users) {
  if (user.active) {
    return user.name;
  }
}

Где это начинает реально болеть

Это не только вопрос читаемости. Дополнительная работа накапливается, когда массивы большие или когда код выполняется в «горячих» участках. А длинные цепочки могут неожиданно раздражать при отладке в продакшене.

Я раньше писал довольно жуткие цепочки. Возвращаться к ним позже —… унизительно.

Плавность не всегда означает ясность

Есть причина, почему цепочки популярны: сначала они читаются красиво.

data
  .transform()
  .normalize()
  .validate()
  .save();

Но теперь вы начинаете гадать, что возвращает каждый шаг, куда вообще вставлять брейкпоинт и можно ли что-то из этого переиспользовать.

Разбиение на шаги сразу отвечает на эти вопросы.

Асинхронные цепочки имеют ту же проблему

Цепочки промисов могут выглядеть изящно:

const data = await fetchUsers()
  .then(res => res.json())
  .then(users => users.filter(u => u.active))
  .then(users => users.map(u => u.name));

Но теперь вы смешиваете асинхронное управление потоком (загрузка, парсинг) с преобразованием данных в одной цепочке.

Разбивать обычно проще и понятнее:

const res = await fetchUsers();
const users = await res.json();
const activeNames = users.filter(u => u.active).map(u => u.name);

Грубое правило, которому я следую

Длина цепочки Рекомендация Пример
1 шаг Вполне нормально users.map(u => u.name)
2 шага Обычно нормально users.filter(u => u.active).map(u => u.name)
3–4 шага Пауза, подумайте о разбиении users.filter(...).map(...).sort(...).slice(...)
5+ шагов Определённо разбиить на шаги Сложные преобразования или асинхронные цепочки

Я не говорю «никогда не цепляйте»

Короткие цепочки — норм. Я всё ещё пишу их. Но как только доходит до трёх-четырёх шагов, я делаю паузу.

Как я думаю об этом сейчас

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

Это не одно и то же.

Как я обычно распутываю такие цепочки

Шаг Что делать Пример
1 Дайте промежуточным значениям имена const activeUsers = users.filter(u => u.active)
2 Логически разделяйте преобразования const names = activeUsers.map(u => u.name)
3 Цепляйте только то, что ясно names.sort()

Это избавило меня от множества головных болей.

JavaScript даёт вам кучу инструментов, но вам не нужно использовать все сразу.

JavaScript