Почему я больше не цепляю (chain) всё подряд в 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(). Проблема в том, что происходит, когда вы их складываете вместе. Вы перестаёте писать шаги и начинаете писать конвейеры (pipelines).
Конвейеры выглядят аккуратно, но всё равно вам приходится мысленно проходить их: 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;
}
}
Где это начинает реально вредить
Это не только про читаемость. Эта лишняя работа накапливается, когда массивы большие или когда код выполняется в «горячих» местах. А длинные цепочки могут неожиданно раздражать при отладке в продакшене.
Я писал раньше довольно жуткие цепочки. Возвращаться к ним позже — это… немного унизительно.
«Плавность» (fluent) не всегда означает ясность
Есть причина, почему цепочки популярны: сначала они читаются приятно.
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