JavaScript 函数式编程入门:纯函数、不可变性与副作用控制
全面解析 JavaScript 函数式编程入门核心思想。围绕纯函数的确定性、不可变数据的操作范式以及副作用的隔离与控制,通过清晰的图解、对比和代码重构案例,帮助开发者写出更稳定、易测试且易维护的代码。
JavaScript 是一门多范式编程语言,允许我们混合使用面向对象、指令式以及函数式编程风格。在规模逐步膨胀的现代前端和 Node.js 项目中,最令开发者头痛的问题往往是“某个变量不知何时被哪个模块篡改了”或“同一个函数在不同时间调用返回了不可预期的结果”。函数式编程(Functional Programming, 简称 FP)提供了一套行之有效的工程约束,其核心思想并非追求抽象晦涩的数学理论,而是通过纯函数保证逻辑确定性、借助不可变性杜绝隐式状态污染,并通过收敛与隔离副作用让代码的运行边界清晰可控。
一、纯函数:确定性与无感知影响
纯函数是函数式编程中最基本的建筑基石。判断一个函数是否为“纯函数”,需要严格满足以下两个核心条件:
- 相同的输入恒产生相同的输出:函数的计算结果完全取决于传入的参数,不依赖任何外部全局变量、系统时间、随机数或外部存储状态。
- 无任何可观察的副作用:函数在执行过程中不会修改入参、不改变外部作用域变量,也不产生未经封装的 I/O 操作(如发起网络请求或写入控制台)。
代码对比:非纯函数 vs 纯函数
以下两个示例展示了计算商品税后金额的具体实现差异:
// ❌ 非纯函数:依赖外部可变变量 taxRate
let taxRate = 0.13;
function calculateTotalImpure(price) {
return price * (1 + taxRate); // 如果外部 taxRate 在其他地方被修改,结果就会突变
}
// ❌ 非纯函数:修改了传入的参数(引发破坏性副作用)
function applyDiscountImpure(cart, discount) {
cart.total = cart.total - discount; // 直接修改入参对象
return cart;
}
// -------------------------------------------------------------
// ✅ 纯函数:所有依赖通过入参明确传入,输入决定唯一输出
function calculateTotalPure(price, currentTaxRate) {
return price * (1 + currentTaxRate);
}
// ✅ 纯函数:不修改原有对象,返回全算出来的崭新对象
function applyDiscountPure(cart, discount) {
return {
...cart,
total: Math.max(0, cart.total - discount)
};
}
关键认知:纯函数拥有“引用透明性”(Referential Transparency)。这意味着如果 calculateTotalPure(100, 0.1) 的结果是 110,你在程序中任何地方用 110 直接替换这行函数调用,程序的行为和最终结果不会发生任何偏差。
二、不可变性:杜绝共享状态与隐式篡改
在 JavaScript 中,原始类型(如 Number、String、Boolean)天然具备不可变性;但对象与数组(Object, Array)是引用类型。多个函数或模块常常共享同一个对象引用,一旦某个分支以指令式的方式直接修改其属性,其他读取该对象的模块就会面临不可预知的“幽灵 Bug”。
不可变性(Immutability)的核心原则是:当状态发生变化时,绝不原地覆写旧数据,而是基于旧数据派生出一份全新的数据副本。
常见变异 API 与不可变替代方案
在日常编写数组与对象处理代码时,需要清楚区分哪些原生方法会“修改原数组/原对象”(变异操作),哪些方法会“返回全新数据”。
| 操作目标 | 变异做法(修改原对象,易产生副作用) | 不可变做法(返回新对象,推荐) |
|---|---|---|
| 追加数组元素 | list.push(item) |
[...list, item] 或 list.concat(item) |
| 移除指定元素 | list.splice(index, 1) |
list.filter((_, i) => i !== index) |
| 修改指定项 | list[index].value = 100 |
list.map((item, i) => i === index ? { ...item, value: 100 } : item) |
| 数组排序/反转 | list.sort() / list.reverse() |
[...list].sort() 或 ES2023 的 list.toSorted() |
| 更新对象属性 | user.age = 28 |
const updatedUser = { ...user, age: 28 } |
三、副作用控制:纯核心与命令式外壳架构
如果一个程序没有任何副作用(不进行网络请求、不写入数据库、不渲染页面 DOM),那么它即使计算得再完美,对外部世界也是毫无意义的。因此,函数式编程不是消灭副作用,而是精确管理并隔离副作用。
业界推崇的成熟模式是“纯核心与命令式外壳”(Pure Core, Imperative Shell)。这种分层思想把应用清晰地切分为两个层面:
- 内部纯核心(Functional Core):包含所有的核心业务规则、状态计算与数据转换。全部由纯函数构建,不执行任何 I/O。
- 外部命令式外壳(Imperative Shell):负责与外部环境打交道,例如监听用户交互、从后端拉取数据、读写 LocalStorage 或操作 DOM。外壳收集到输入后,交由纯核心进行无副作用计算,获得结果后再集中执行副作用落地。
四、重构实战:购物车结算业务代码改造
下面通过一个常见的“购物车优惠券应用与总价结算”业务场景,展示从传统的命令式副作用混杂代码,逐步重构成函数式清晰分层的完整过程。
1. 原始做法:逻辑、I/O 与变异状态紧密耦合
// ❌ 存在隐患的指令式写法:全局混杂、直接变异数据、混入网络与 DOM 操作
let cartItems = [
{ id: 1, name: "键盘", price: 300, quantity: 1 },
{ id: 2, name: "鼠标", price: 150, quantity: 2 }
];
async function checkoutImperative(couponCode) {
// 1. 发起网络请求(副作用)
const res = await fetch(`/api/coupon?code=${couponCode}`);
const coupon = await res.json();
// 2. 直接修改全局数组内的对象(变异原数据)
for (let i = 0; i < cartItems.length; i++) {
if (cartItems[i].price > 200) {
cartItems[i].price = cartItems[i].price - coupon.discount; // 破坏性修改!
}
}
// 3. 计算总价
let total = 0;
for (let item of cartItems) {
total += item.price * item.quantity;
}
// 4. 直接修改 DOM 与 本地存储(分散的副作用)
document.getElementById("total-price").innerText = total;
localStorage.setItem("cached_cart", JSON.stringify(cartItems));
}
2. 函数式重构:提炼纯核心,将副作用推向边界
重构时,我们首先将“优惠券折扣应用”和“总价汇总”提炼为纯粹的计算函数,保持原数据不变;将 fetch、DOM 操作、localStorage 统一放置在最外层的调用函数中。
// ✅ 纯函数 1:计算单项商品应用折扣后的状态(不可变操作)
function calculateItemDiscount(item, discountThreshold, discountAmount) {
if (item.price <= discountThreshold) {
return item; // 未达到优惠门槛,原样返回
}
return {
...item,
price: Math.max(0, item.price - discountAmount) // 产生新对象,不污染原数据
};
}
// ✅ 纯函数 2:对列表进行批量折扣派生(纯管道)
function applyCartCoupon(items, discountThreshold, discountAmount) {
return items.map(item => calculateItemDiscount(item, discountThreshold, discountAmount));
}
// ✅ 纯函数 3:纯数学累加计算总价
function calculateTotalPrice(items) {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
// -------------------------------------------------------------
// 🌍 命令式外壳(Imperative Shell):只负责调度副作用与编排纯逻辑
async function checkoutFunctional(initialCart, couponCode) {
try {
// 步骤 A:从网络边界获取输入(副作用)
const res = await fetch(`/api/coupon?code=${couponCode}`);
const coupon = await res.json();
// 步骤 B:调用纯核心函数完成业务推导(确定性计算,极易单测)
const discountedCart = applyCartCoupon(initialCart, 200, coupon.discount);
const finalTotal = calculateTotalPrice(discountedCart);
// 步骤 C:在边界集中执行输出副作用
document.getElementById("total-price").innerText = finalTotal;
localStorage.setItem("cached_cart", JSON.stringify(discountedCart));
// 返回新状态供上层流转
return { success: true, cart: discountedCart, total: finalTotal };
} catch (error) {
console.error("结算流程出现异常:", error);
return { success: false, error };
}
}
重构带来的收益:测试 calculateItemDiscount 和 calculateTotalPrice 时,不需要搭建任何 Mock 服务器,不需要构建虚拟 DOM,也不需要清理 localStorage,只需要直接传入固定数据断言返回值即可,单元测试编写耗时降低 80% 以上。
五、函数式三大要素协同全景总结
在 JavaScript 日常开发中,并不需要过度追求高阶代数结构(如 Monad、Functor 等高深数学抽象)。掌握并实践“纯函数”、“不可变性”和“副作用控制”这三大要素,就已经能够解决项目中 90% 以上的可维护性痛点。
结语
函数式编程在 JavaScript 生态中并不是一个全有或全无的选择。现代前端框架(如 React 的状态不可变更新、Redux 的纯函数 Reducer、Vue 的无副作用计算属性)处处体现着函数式编程的智慧。从今天开始,在编写每一个新函数时,先思考三个问题:“它能否不依赖外部变量?”、“它是否修改了入参?”以及“能否将它的 I/O 操作推到调用方?”,你的代码结构便会自然走向清晰、稳固与优雅。
JavaScript怎么遍历对象的Symbol键和不可枚举属性
2026-11-05 13:13:39
封装一个健壮的JS字符串判空工具函数
2026-11-05 12:16:14
JavaScript合并数组的三种常用方法:concat、扩展运算符与push
2026-11-05 11:22:29
JavaScript立即执行函数(IIFE)是什么及基本语法
2026-11-05 10:51:19
JS对象数组转字符串为什么推荐用JSON.stringify()
2026-11-05 09:40:31








