JavaScript 的秘密生活:函数组合的力量

发布: (2025年12月20日 GMT+8 11:57)
10 min read
原文: Dev.to

I’m happy to translate the article for you, but I’ll need the full text you’d like translated. Could you please paste the content (excluding the source line you already provided) here? Once I have the text, I’ll translate it into Simplified Chinese while preserving the original formatting, markdown, and any code blocks or URLs.

Source:

问题

Timothy 心情不佳。他盯着一段代码,看起来更像是一次交通事故后的数学方程,而不是编程。

“我讨厌这个,”他嘀咕道。

Margaret 在他的桌旁停下,挑了挑眉。

“‘讨厌’是个强烈的词,Timothy。代码到底对你做了什么?”

“它是倒着读的!”Timothy 指着屏幕说。“我写了这些很好的小助手函数来格式化用户数据,但把它们组合在一起简直是噩梦。就像剥洋葱一样,但洋葱让我哭。”

他高亮了代码:

const userInput = "   gandalfthegrey  ";

// My helper functions
const trim      = str => str.trim();
const upperCase = str => str.toUpperCase();
const exclaim   = str => `${str}!`;
const bold      = str => `**${str}**`;

// The "Onion" of code
const onionResult = bold(upperCase(exclaim(trim(userInput))));

console.log(onionResult); // "**GANDALFTHEGREY!**"

“啊,”Margaret 点头说。“这就是 Inside‑Out(内外)问题。你想先去掉字符串两端的空格,然后转成大写,再加上感叹号,最后加粗。但必须把它们写成相反的顺序。”

“如果我想再加一步怎么办?”Timothy 问道。“我得找出那个嵌套的正中间,把它塞进去。太乱了。”

“确实,”Margaret 同意道。“你在为计算机写代码,而不是为人写代码。计算机喜欢嵌套, 人类喜欢顺序。我们需要把那个洋葱变成管道。”

概念:管道

Margaret 抓起一支记号笔,在白板上快速画了一个图,比较这两种方法。

The "Onion" (Nested)      vs.      The Pipeline (Composed)
Reads Inside‑Out                   Reads Top‑to‑Bottom

      bold(                         "  data  "
        upperCase(                      |
          trim(                         V
            " data "               +----------+
          )                        |   trim   |
        )                          +----------+
      )                                 |
                                        V
                                   +-----------+
                                   | upperCase |
                                   +-----------+
                                        |
                                        V
                                   +----------+
                                   |   bold   |
                                   +----------+
                                        |
                                        V
                                    "**DATA**"

“想象你的数据是水,”她解释道,指着图的右侧。“函数组合就像管道。我们把一个函数的输出直接连接到下一个函数的输入。数据就会顺畅地从上到下流动。”

“看起来好多了,”Timothy 承认道。“但 JavaScript 本身没有管道。”

“它今天没有内置的管道运算符,”Margaret 说明。“但我们可以自己实现这种行为。这是函数式编程的超能力之一。”

Source:

构建 pipe

Margaret 打开了一个新文件。

“我们需要一个函数,接受一个函数列表并按顺序执行它们。先写一个叫 pipeLoop 的版本,这样你就能准确看到发生了什么。”

// The "Boring" (but clear) Pipe
const pipeLoop = (...functions) => {
    return (initialValue) => {
        let result = initialValue;

        // Loop through every function in the list
        for (let func of functions) {
            // Pass the result of the last function into the next one
            result = func(result);
        }

        return result;
    };
};

“这只是一个循环,” Margaret 解释道。“它拿你的初始值,传给第一个函数,然后把 那个 结果传给下一个函数,一直循环下去,直到函数用完。它就像装配线一样。”

Timothy 点点头。“好,这很容易理解。就是不断更新 result 变量。”

“没错,” Margaret 说。“因为我们是 JavaScript 开发者,又喜欢一行代码的写法,通常会用 reduce 来写成这样。它的功能和 pipeLoop 完全相同。既然两者等价,我们就在真实代码中使用这个更简洁的 pipe 版本:”

// The "Pro" Pipe
const pipe = (...functions) => (initialValue) =>
    functions.reduce(
        (currentValue, currentFunction) => currentFunction(currentValue),
        initialValue
    );

她用这个新工具重构了 Timothy 那段凌乱的代码:

// Step 1: Reuse the small functions (trim, upperCase, exclaim, bold) you defined earlier.

// Step 2: Create the pipeline
const formatString = pipe(
    trim,
    upperCase,
    exclaim,
    bold
);

// Step 3: Use it
const result = formatString("   gandalfthegrey  ");
console.log(result); // "**GANDALFTHEGREY!**"

“哇,” Timothy 说。“这读起来就像英文一样。Trim、Uppercase、Exclaim、Bold。自上而下。”

但是我该如何调试它?

Timothy 轻轻皱了皱眉。

“在我的‘洋葱’代码中,我可以直接在中间 console.log 变量来查看发生了什么。而这里,数据被隐藏在管道里。我看不到它。”

“我们来创建一个窗口,”Margaret 说,写下了一个小的辅助函数:

const trace = label => value => {
    // Use comma separation so objects log correctly
    console.log(label, value);
    return value;
};

“它只会记录该值并原样返回。现在你可以在不破坏管道的情况下监视你的数据:”

const formatString = pipe(
    trace('after trim'),   // “Okay, that is actually incredibly useful.”
);

“啊哈!”的连接:为什么我们需要柯里化

“现在来最后一步,”玛格丽特说。“这对 trimupperCase 很有效,因为它们只接受一个参数。但如果我想使用一个标准的两参数加法函数怎么办?”

他敲出了一个例子:

// A standard function
const standardAdd = (a, b) => a + b;
const double = x => x * 2;

// This won't work in a pipe! 
const newNumber = pipe(
    double,
    standardAdd // Problem: expect (a, b), but pipe only gives it 'a'
)(5); 

console.log(newNumber); // NaN (because 'b' was undefined!)

玛格丽特又快速画了一个草图来说明问题。

管道是标准连接器——它要求每个环节恰好有一个输入和一个输出。

The Problem: A mismatched pipe connection

[Previous Func] outputs: 5
       |
       V
(Pipe expects 1 input) -> +-------------------+
                          | standardAdd(a, b) |  +-------------------+

柯里化作为适配器

“现在你明白我上周让你学习 柯里化 的原因了吧。”

“它是适配器?”蒂莫西问。
“正是!”玛格丽特说。“柯里化让我们可以预先填充参数,直到函数只需要 一个 参数——即管道中传下来的那个值。”

The Solution: Currying as an Adapter

[Previous Func] outputs: 5
       |
       V
+-----------------+
|    add(10)      |  b => a + b;

// We reuse 'double' from the previous example

// Now we can "pre‑load" the add function
// add(10) returns a function that waits for the second number
const processNumber = pipe(
    double,    // 5 * 2 = 10
    add(10),   // 10 comes down the pipe. 10 + 10 = 20
    double    // 20 * 2 = 40
);

console.log(processNumber(5)); // 40

Margaret的速查表

何时使用组合(管道):

  • 数据转换: 当你拥有原始数据(例如,一个用户对象)需要经过多步“打磨”(sanitize → validate → normalize)时。
  • 可读性: 当你发现自己嵌套函数的层数超过三层(f(g(h(x))))时。
  • 重构: 当你看到一个“上帝函数”在做十件事时。将其拆分为十个小函数并使用 pipe 将它们串联起来。

关于异步的警告:

这个简单的 pipe 函数是同步的。如果你的函数返回 Promise(即它们是 async),管道将会中断,因为下一个函数会收到一个 Promise 而不是普通值。

术语表

  • Unary Function(一元函数): 接受恰好一个参数的函数。理想情况下,管道中的所有函数都应该是一元的。
  • Pure Function(纯函数): 对相同输入始终返回相同输出且不产生副作用(例如,修改全局变量)的函数。
  • Currying(柯里化): 将具有多个参数的函数转换为一系列单参数函数的过程。它是让复杂函数适配管道的“适配器”。

Aaron Rose 是一名软件工程师兼技术作家,供职于 tech-reader.blog ,也是《Think Like a Genius》的作者。

Back to Blog

相关文章

阅读更多 »

JavaScript 中的函数组合

介绍 函数组合(functional composition),也称为函数管道(function pipelines),让你能够将简单函数链式连接,以创建更易读且模块化的代码。定义…

JavaScript 中的一等函数

介绍 对于学习 JavaScript 的开发者来说,术语 first‑class functions 在讨论和文档中经常出现。在 JavaScript 中,函数 a...