Skip to content

纯函数:不是“消除”副作用,而是“驱逐”副作用

——函数式编程的“净化仪式”

“纯函数不消灭副作用,它只是把混乱赶出圣殿,圈养在围墙之外。”

一、重新定义问题:纯函数 ≠ 消除副作用

你的原句: “纯函数:相同输入 → 相同输出 + 无外部影响”

这描述的是结果,但没回答机制

更准确的说法是:

纯函数通过“不依赖也不改变任何外部状态”,将副作用排除在函数体之外。

换句话说:
纯函数不是“处理”了副作用,而是“拒绝容纳”副作用。

就像一个无菌实验室——它本身不产生污染,所有污染物都被挡在门外。

二、机制剖析:纯函数如何“物理隔离”副作用?

我们从 JS 引擎的执行模型来看:

1. 执行上下文(Execution Context)的纯净性

当一个纯函数执行时,它的词法环境(Lexical Environment)只包含:

  • 参数(入栈)
  • 局部变量(在栈帧内)
  • 返回值(出栈)
js
function add(a, b) {
  const temp = a + b;     // 仅操作局部变量
  return temp;            // 返回值唯一出口
}

没有闭包引用外部可变变量
没有访问全局对象(window, globalThis)
没有 mutation 外部数据结构

这意味着:函数的整个生命周期,都在自己的“沙盒”中完成

2. 副作用的“入侵路径”被全部封锁

副作用类型纯函数如何防御
全局变量修改不引用任何自由变量(free variable)
DOM 操作不调用 document, querySelector
网络请求不调用 fetch, XMLHttpRequest
时间依赖不调用 Date.now(), new Date()
随机数不调用 Math.random()
日志打印不调用 console.log
异常抛出不使用 throw(或仅用于逻辑错误,而非控制流)

纯函数的“防御机制”就是:什么都不做,除了计算。

三、副作用去哪了?——它们被“放逐”到系统边缘

纯函数不能有副作用,但程序必须有副作用(否则就是个计算器)。

解决方案是:分层架构 —— 核心逻辑纯化,边缘处理副作用

这就是 The Functional Core, Imperative Shell 模式。

示例:用户注册流程

js
// 纯函数:决策逻辑
function validateUser(user) {
  if (!user.email) return { valid: false, error: 'Email required' };
  if (user.age < 18) return { valid: false, error: 'Underage' };
  return { valid: true, user: { ...user, registeredAt: null } };
}

function createUserRecord(user) {
  return { ...user, id: generateId(), registeredAt: null };
}

// 不纯:副作用集中在外层
async function registerUser(rawUser) {
  const result = validateUser(rawUser);
  if (!result.valid) throw new Error(result.error);

  const user = createUserRecord(result.user); // 纯函数调用

  console.log('Registering:', user);          // 日志
  const saved = await db.save(user);          // 数据库
  await emailService.sendWelcome(saved);      // 邮件

  return saved;
}
  • 内核(Core)validateUser, createUserRecord 是纯函数,可测试、可缓存
  • 外壳(Shell)registerUser 处理所有副作用,但它本身不再包含复杂逻辑

纯函数把副作用“推”到了最外层,让核心业务逻辑保持可推理。

四、为什么这能提升可维护性?——副作用的成本模型

副作用不是“错误”,而是高成本操作。纯函数通过“驱逐”它们,降低了系统的认知负荷

维度副作用代码纯函数代码
测试需要 mock DB、网络、时间直接传参断言,无需外部依赖
调试错误可能来自任意外部调用错误只来自输入或逻辑,定位极快
重构改动可能破坏隐式依赖可安全重命名、移动、组合
并行可能竞争共享状态可安全多线程执行
缓存不可用可 memoize,避免重复计算

纯函数的本质优势:它把“不确定性”转化为“确定性”,把“运行时风险”转化为“编译时可验证”。

五、JS 中的“伪纯函数”陷阱再探

陷阱 3:对象参数的隐式共享

js
function getTotal(items) {
  return items.reduce((sum, item) => sum + item.price, 0);
}

const cart = [{ price: 10 }, { price: 20 }];
getTotal(cart); // 30

// 但如果另一个函数偷偷改了 cart?
cart[0].price = 100;
getTotal(cart); // 120 —— 即使输入“看起来”一样,输出变了!

问题:虽然 getTotal 逻辑上是纯的,但由于对象引用共享,它实际上依赖了外部可变状态。

解决方案:深冻结或不可变数据

js
// 开发期防御
function pureGetTotal(items) {
  const frozen = items.map(Object.freeze);
  return frozen.reduce((sum, item) => sum + item.price, 0);
}

或使用 Immutable.js / Immer。

六、工程实践:如何渐进式“净化”现有代码?

  1. 识别“纯逻辑片段”
    把 if/else 中的判断条件提取为纯函数:

    js
    // 原始
    if (user.age >= 18 && user.active) { }
    
    // 提取
    const isEligible = (user) => user.age >= 18 && user.active;
  2. 用纯函数替代状态突变

    js
    // 命令式
    users.push(newUser);
    
    // 函数式
    const updatedUsers = [...users, newUser];
  3. 用返回值代替副作用控制流

    js
    // 不纯
    function handleLogin(user) {
      if (!user.email) {
        showError('Email missing');
        return;
      }
      // ...
    }
    
    // 更纯
    function validateLogin(user) {
      if (!user.email) return Left('Email missing');
      return Right(user);
    }

结语:纯函数是“副作用的防火墙”

纯函数的价值,不在于它“做了什么”,而在于它“拒绝做什么”。

它像一道防火墙,将程序的核心逻辑与外部世界的混沌隔离开来。

当你用纯函数构建系统,你就在创造一个可预测、可测试、可组合、可推理的代码宇宙。

这才是函数式编程真正提升可维护性的秘密:

它不消除副作用,而是让副作用变得显式、可控、可管理。