Почему я больше не цепляю всё подряд в JavaScript — Мэтт Смит
Перевод статьи: Why I don't chain everything in JavaScript anymore - Matt Smith
Я раньше писал много 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 даёт вам много инструментов, но вам не нужно использовать их все сразу.