Почему я больше не цепляю всё подряд в 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 даёт вам кучу инструментов, но вам не нужно использовать все сразу.
JavaScript