我不再用 JavaScript 将所有东西串联起来。
I don't chain everything in JavaScript anymore

原始链接: https://allthingssmitty.com/2026/04/20/why-i-dont-chain-everything-in-javascript-anymore/

虽然 JavaScript 链式调用(`.filter().map().sort()`)最初看起来简洁易读,但随着复杂度的增加,它会变得难以管理和调试。作者认为,过长的链条会模糊数据流,迫使开发者在脑海中“走查”整个流程,从而阻碍理解和维护。 相反,将操作分解为独立的、命名的步骤——即使这意味着更多的代码行——可以提高清晰度。每个步骤都变得自包含且易于检查。这种方法在调试时尤其有益,避免了在链条*内部*插入 `console.log` 语句的需求。 此外,链式调用可能导致不必要的数据处理。当只需要一个结果时,使用诸如 `.find()` 或简单的 `for` 循环通常能提供更高效的解决方案。 一个通用准则:短链(1-2 步)是可以接受的,但超过 3-4 步时,应考虑将其分解。优先考虑代码的可读性和可维护性,而不是简洁性,尤其是在处理复杂转换或异步操作时。链式调用适用于快速原型设计,但分解步骤可以改善代码的长期健康状况。

## Hacker News 讨论:重新思考 JavaScript 链式调用 一篇最近的 Hacker News 文章引发了关于 JavaScript 中方法链式调用的讨论。作者认为他们已经减少了大量的链式调用,转而使用更明确、多行的代码以提高可读性和可维护性。 许多评论者表示同意,指出虽然链式调用*看起来*简洁,但它可能会阻碍调试,并使代码更难理解,尤其是在复杂的运算中。一个关键点是,链式调用会模糊数据流,并鼓励过度复杂的“管道”结构。 几位用户强调了描述性函数名称的重要性,而不是依赖链式调用来追求简洁。 另一些人指出,较新的 JavaScript 特性,如惰性迭代器,可以作为解决链式调用一些批评的潜在方案。 讨论还涉及了函数式和面向对象编程风格之间的权衡,以及混合范式时可能产生的认知负荷。 一种普遍的观点是,随着开发人员经验的积累,优先考虑清晰、自文档化的代码至关重要。 一些评论员也注意到,AI 生成的内容缺乏深度有所增加,类似于这篇文章。
相关文章

原文

I used to write a lot of JavaScript like this:

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

Nothing here is wrong. I wrote code like this all the time. But this is exactly the kind of thing that feels fine at first, then slowly gets harder to work with.

Chaining is great…until it isn’t

The issue isn’t .map() or .filter(). It’s what happens when you stack them. You stop writing steps and start writing pipelines.

Pipelines look clean, but you still have to walk through them in your head: filter → map → sort → slice.

That’s fine once or twice. Do it all over a file and it starts to wear on you.

Compare that to this:

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

names.sort();

return names.slice(0, 5);

Yeah, it’s more lines. But each step is just sitting there. No decoding required.

Same problem, three ways to write it

Here’s the same intent written three different ways.

If I’m chaining:

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

It looks neat. I used to reach for this a lot. But it processes everything, even though I only need one result.

If I’m writing it in steps:

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

This is usually where I land. It stops early, and if something feels off I can check each piece.

If I want full control:

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

This is the most explicit and, honestly, sometimes the clearest when I really care about what’s happening.

Where things get a little messy

This shows up fastest when you try to debug.

Say something feels off and you want to check the filtered results. With a chain, you end up doing this:

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

Now your logic is mixed with debugging code. Or you give up and break the chain apart anyway.

You can end up doing more work than you need

Chaining nudges you toward “process everything,” even when that’s not what you meant to do.

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

This filters the entire array, maps the result, and then grabs one item.

When what you actually wanted was:

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

Or:

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

Where this starts to hurt

This isn’t just about readability. That extra work adds up with large arrays or hot paths. And long chains can be surprisingly annoying to debug in production.

I’ve written some pretty gnarly chains before. Coming back to them later is…humbling.

Fluent doesn’t always mean clear

There’s a reason chaining is popular: it reads nicely at first.

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

But now you’re wondering what each step returns, where you’d even put a breakpoint, or whether any of it is reusable.

Breaking it into steps answers those questions right away.

Async chains have the same problem

Chaining promises can look sleek:

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

But now you’re mixing async control flow (fetching, parsing) with data transformation in one chain.

Splitting it up is usually easier to follow:

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

A rough rule I follow

Chain length Recommendation Example
1 step Perfectly fine users.map(u => u.name)
2 steps Usually fine users.filter(u => u.active).map(u => u.name)
3–4 steps Pause, consider breaking up users.filter(...).map(...).sort(...).slice(...)
5+ steps Definitely break into steps Complex transformations or async chains

I’m not saying never chain

Short chains are fine. I still write them. Once I hit three or four steps, I pause.

How I think about this now

Chaining is great when you’re writing code quickly. Breaking things into steps is better when that code has to be read later.

Those aren’t the same thing.

How I usually untangle these

Step What to do Example
1 Name intermediate values const activeUsers = users.filter(u => u.active)
2 Separate transformations logically const names = activeUsers.map(u => u.name)
3 Only chain what’s clear names.sort()

This has saved me from a lot of headaches.

JavaScript gives you a lot of tools, but you don’t need to use all of them at once.

联系我们 contact @ memedata.com