作为一名开发者,你是否曾在调试一个复杂的 Bug 时,渴望能有一种方法让代码的行为变得可预测?或者在重构老旧代码时,希望某段逻辑能像数学公式一样,无论放在哪里运行都得到一致的结果?这正是函数式编程致力于解决的核心问题之一。而在 2026 年,随着 AI 辅助编程(如 Cursor、Windsurf 等)的普及,这种对代码确定性的需求不仅没有降低,反而变得前所未有的重要。为什么?因为 AI 模型(LLM)在生成和重构代码时,最怕的就是隐式依赖和隐藏的状态突变。
在这篇文章中,我们将不再停留于枯燥的定义,而是像两个工程师在白板前讨论一样,深入探索 JavaScript 中这两种函数的本质区别。我们会剖析它们的工作原理,探讨如何编写更易于测试和维护的代码,并通过大量的实战案例,让你在实际项目中游刃有余地运用这些概念。我们还会特别探讨在“AI 原生”开发时代,纯函数如何成为我们与 AI 协作的通用语言。
纯函数:构建可靠代码的基石
首先,让我们来聊聊纯函数。你可以把纯函数想象成一个完美的数学公式。在数学中,无论你何时计算 INLINECODEcfef36f6,结果永远是 INLINECODE3c2bed37。它不会因为今天是星期二,或者因为计算机的内存变了,就变成 8。
什么是纯函数?
从技术上讲,一个函数要成为“纯”的,必须满足两个严格的条件:
- 引用透明性: 对于相同的输入,永远返回相同的输出。
- 无副作用: 函数的执行不会改变外部世界的状态(比如不修改全局变量,不进行网络请求,不操作 DOM)。
为什么要坚持写纯函数?
你可能会问:“为什么要这么死板?”实际上,这种死板带来了巨大的工程价值:
- 极易测试: 你不需要设置复杂的数据库环境或模拟网络请求,只需传入参数,断言输出结果。
- 可缓存性: 因为输入决定输出,我们可以轻松地存储计算结果(记忆化),极大提升性能。
- 自文档化: 纯函数的所有依赖都通过参数传入,你不需要阅读函数体内的代码就能知道它依赖什么。
- AI 友好性(2026 视角): 这一点是近两年才凸显出来的。当我们使用 LLM 进行代码生成或重构时,纯函数由于不依赖外部上下文,AI 可以更准确地理解其意图,甚至将其作为模块直接移植到不同的项目中,而不会产生“上下文缺失”的错误。
让我们看一个纯函数的示例
下面是一个简单的纯函数,它计算两个数字的和。这虽然简单,但它完美地展示了确定性。
// 这是一个纯函数:
// 1. 引用透明:输入 3, 4 总是得到 7
// 2. 无副作用:它不修改外部变量,也不依赖外部状态
function add(a, b) {
return a + b;
}
console.log(add(3, 4)); // 输出: 7
console.log(add(3, 4)); // 输出: 7 (无论调用多少次,结果不变)
在这个例子中,INLINECODEfd31fd12 函数完全是一个封闭的系统。它不关心外界发生了什么,只关心传入的 INLINECODEd3f7e810 和 b。这种隔离性是构建健壮系统的关键。
实战案例:数据处理中的纯函数
让我们看一个稍微复杂一点的例子。假设我们正在处理一个用户列表,我们需要根据年龄筛选用户。
const users = [
{ id: 1, name: "Alice", age: 25 },
{ id: 2, name: "Bob", age: 17 },
{ id: 3, name: "Charlie", age: 19 }
];
// 这是一个纯函数:它接受输入,返回新数据,不修改原始数组
function getAdultUsers(userList) {
// 使用 filter 创建一个新数组,原 users 数组保持不变
return userList.filter(user => user.age >= 18);
}
const adults = getAdultUsers(users);
console.log(adults); // 输出: [Alice, Charlie]
console.log(users[1].age); // 输出: 17 (原始数据未被修改)
在这里,我们避免了直接修改 users 数组。这种“不修改原始数据”的习惯,能让你在开发复杂应用时避免难以追踪的状态变更 Bug。如果在 2026 年,你让 AI 帮你优化这段代码,它绝对不会因为担心副作用而犹豫,因为逻辑非常清晰。
非纯函数:副作用与外部依赖
理解了纯函数之后,非纯函数的概念就很简单了:任何不满足纯函数定义的函数,就是非纯函数。这通常意味着它们要么依赖外部状态,要么会产生副作用。
什么是副作用?
“副作用”这个词听起来像是个贬义词,但在编程中,它只是指函数除了返回值之外,还对系统其他部分产生了影响。常见的副作用包括:
- 修改全局变量。
- 修改传入的参数对象(引用传递)。
- 发起 HTTP 请求。
- 操作 DOM(例如
document.getElementById)。 - 获取随机数或当前时间。
- 读写文件系统或数据库。
为什么非纯函数难以维护?
非纯函数并不是“坏”的(毕竟我们需要通过它们与外界交互),但它们确实带来了风险。因为它们的行为可能受到外部环境的影响,单元测试变得困难,且代码逻辑变得难以追踪。
示例 1:修改全局变量的陷阱
让我们看一个最典型的反面教材。假设我们有一个计数器逻辑。
let count = 0; // 这是一个全局状态
// 这是一个非纯函数
// 原因:它依赖于并修改了函数作用域之外的变量 count
function incrementCounter() {
count += 1;
return count;
}
console.log(incrementCounter()); // 输出: 1
console.log(incrementCounter()); // 输出: 2 (相同的输入(无参数),输出却不同)
在实际开发中,这种模式会导致“竞态条件”。如果你的代码的另一部分也修改了 INLINECODE92b86504,INLINECODE5bde59ae 的结果就变得不可预测了。
解决方案: 我们可以通过将状态作为参数传入,将其转化为纯函数。
// 重构后的纯函数版本
function incrementCounter(currentCount) {
return currentCount + 1;
}
// 状态的管理交由调用者控制
let newState = incrementCounter(0); // 1
newState = incrementCounter(newState); // 2
示例 2:修改对象状态
在 JavaScript 中,对象是按引用传递的。这意味着如果你直接修改传入对象的属性,你实际上是在修改外部的那块内存。
const userProfile = {
username: "dev_ninja",
score: 100
};
// 这是一个非纯函数
// 它直接修改了传入对象的内容,产生了副作用
function doubleScore(user) {
user.score *= 2; // 直接修改了外部对象
}
doubleScore(userProfile);
console.log(userProfile.score); // 输出: 200 (原始对象被污染了)
这在大型应用中是非常危险的。你可能以为 userProfile 还是原来的样子,但它已经被某个函数悄悄改变了。
最佳实践: 使用不可变更新模式。我们可以返回一个新对象,而不是修改旧对象。
// 使用展开运算符(...) 创建对象副本
function doubleScorePure(user) {
// 返回一个新对象,原对象保持不变
return { ...user, score: user.score * 2 };
}
const newProfile = doubleScorePure(userProfile);
console.log(userProfile.score); // 100 (原数据安全)
console.log(newProfile.score); // 200 (新数据正确)
2026 年架构设计:构建“三明治”式的应用结构
既然非纯函数不可避免(我们总需要修改 DOM、发请求、存数据),那么在 2026 年的现代开发中,我们应该如何组织代码呢?核心策略是:隔离非纯代码,最大化纯代码的比例。 这在现代前端架构(如 React 服务器组件、Vue 3.5+ 的 Composition API)中尤为关键。
我们可以将应用架构想象成一个“三明治”:
- 顶层:Impure Layer(非纯层,UI 与胶水代码)。这里处理 DOM 操作、用户事件监听、网络请求获取数据。这一层很“脏”,但很薄。
- 中间层:Pure Core(纯核心,业务逻辑)。这里包含了所有的算法、数据转换、状态计算逻辑。这一层非常“干净”,是应用最厚实的部分,也是 AI 最能发挥作用的领域。
- 底层:Infrastructure(基础设施,持久化)。数据库操作、文件系统交互。
实战案例:构建一个智能数据转换器
让我们假设我们需要编写一个功能:从 API 获取用户数据,根据用户的 VIP 等级计算折扣后的价格,并更新 UI。
错误的写法(全部混杂在一起):
// 这是一个难以维护、难以测试、AI 也难以重构的非纯函数
async function updatePriceDisplay(userId) {
// 1. 副作用:网络请求
const user = await fetch(`/api/users/${userId}`).then(res => res.json());
// 2. 副作用:读取外部配置 (可能是一个全局变量)
const basePrice = window.globalConfig.basePrice;
// 3. 纯计算逻辑(混在里面)
let discount = 1;
if (user.level === ‘VIP‘) discount = 0.8;
if (user.level === ‘SVIP‘) discount = 0.6;
const finalPrice = basePrice * discount;
// 4. 副作用:直接操作 DOM
document.getElementById(‘price‘).innerText = `$${finalPrice}`;
}
这种代码在 2026 年依然是常见的噩梦。如果你想测试折扣逻辑,你必须 mock INLINECODE4cc2fa0f, mock INLINECODE06ad5907,甚至还要设置一个假的 DOM 环境。
2026 年推荐的最佳实践(分层架构):
我们将逻辑拆分为三个部分。
第一部分:纯函数(业务核心)
/**
* 计算折扣后的价格
* 这是一个纯函数,非常适合单独测试和 AI 理解
* @param {number} basePrice - 基础价格
* @param {string} userLevel - 用户等级
* @returns {number} 折扣后价格
*/
function calculateDiscountedPrice(basePrice, userLevel) {
// 我们甚至可以把折扣规则提取为配置对象,使逻辑更清晰
const discounts = {
‘NORMAL‘: 1.0,
‘VIP‘: 0.8,
‘SVIP‘: 0.6
};
const multiplier = discounts[userLevel] || 1.0;
return basePrice * multiplier;
}
// 单元测试示例
// console.log(calculateDiscountedPrice(100, ‘VIP‘)); // 输出: 80
第二部分:非纯函数(副作用处理)
/**
* 获取用户数据 (副作用:网络请求)
*/
async function fetchUserData(userId) {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
/**
* 更新 UI (副作用:DOM 操作)
*/
function renderPrice(price) {
const priceElement = document.getElementById(‘price‘);
if (priceElement) {
priceElement.innerText = `$${price}`;
}
}
第三部分:胶水代码(组合它们)
/**
* 主控制器:协调副作用与纯逻辑
* 这也是 React 组件或 Vue Composable 内部做的事情
*/
async function handlePriceUpdate(userId, basePrice) {
try {
// 1. 获取数据
const user = await fetchUserData(userId);
// 2. 执行纯逻辑计算 (核心)
const finalPrice = calculateDiscountedPrice(basePrice, user.level);
// 3. 渲染结果
renderPrice(finalPrice);
} catch (error) {
console.error("更新价格失败:", error);
// 错误处理也是一种副作用
}
}
这种结构的优势在于:
- 可测试性: 你可以单独测试
calculateDiscountedPrice,无需启动服务器或浏览器。 - 可复用性:
calculateDiscountedPrice可以被复制到后端、移动端,或者给其他微服务使用。 - AI 协作友好: 如果你使用 Cursor 的“Tab”键自动补全,或者让 AI 帮你解释代码,纯函数部分因为它明确的输入输出,解释的准确率远高于混杂的代码。
深入探究:副作用容器与单子思想
作为 2026 年的开发者,你可能听说过“单子”这个概念。虽然 JavaScript 不是 Haskell,但我们可以借鉴其思想来处理副作用。最常见的两个例子是处理异步操作的 Promise 和处理空值安全的 Maybe 模式。
1. Promise:异步副作用的管理
Promise 本质上就是一个副作用容器。它并不执行副作用,而是承诺将来会执行。这使得我们可以像处理数据一样处理 IO 操作。
// Promise 允许我们使用 .then 将非纯操作串联起来,保持一定的代码洁癖
fetch(‘/api/data‘)
.then(response => response.json()) // 转换数据 (纯函数)
.then(data => data.filter(item => item.isActive)) // 过滤数据 (纯函数)
.then(filteredData => updateUI(filteredData)); // 更新 UI (非纯)
2. Maybe 模式:优雅地处理空值
在非纯世界中,INLINECODE839f839b 和 INLINECODE1231c56b 是最令人头疼的副作用之一(隐形的数据缺失)。我们可以编写一个简单的容器来封装这种不确定性。
// 一个简单的 Maybe 容器类
class Maybe {
constructor(value) {
this.$value = value;
}
static of(value) {
return new Maybe(value);
}
isNothing() {
return this.$value === null || this.$value === undefined;
}
// map 方法:如果值存在,则执行函数;否则跳过
map(fn) {
return this.isNothing() ? Maybe.of(null) : Maybe.of(fn(this.$value));
}
}
// 使用场景:获取用户的街道名称
const user = { address: { street: "编程大道", city: "深圳" } };
// 常规写法 (容易崩溃)
// const street = user.address.street;
// Maybe 写法 (纯函数链式调用)
const getStreetName = (user) => {
return Maybe.of(user)
.map(u => u.address)
.map(addr => addr.street)
.$value; // 提取最终值
};
console.log(getStreetName(user)); // "编程大道"
console.log(getStreetName(null)); // null (安全,不报错)
这种模式把“检查空值”这个非纯粹的、依赖上下文的行为,封装在了 map 方法内部,让我们的业务逻辑保持纯粹。
总结与展望
在这篇文章中,我们深入探讨了 JavaScript 中纯函数与非纯函数的世界。我们看到了纯函数是如何通过确定性和无副作用帮助我们编写出更安全、更易于测试的代码。同时,我们也承认非纯函数在处理 I/O 和状态变更时的必要性。
随着我们迈向 2026 年及以后,软件开发的复杂性只增不减。边缘计算、Serverless 架构要求我们的代码更加模块化和无状态;AI 辅助编程要求我们的代码逻辑更加清晰、上下文依赖更少。在这些背景下,函数式编程不再是一种“学术追求”,而是构建现代化、可维护系统的基础。
作为一名开发者,你的目标不是完全消灭非纯函数,而是要清晰地划分界限。试着让你的核心逻辑由纯函数组成,只在必要的地方通过非纯函数与外部世界交互。
接下来的步骤建议:
- 审查现有代码: 找出你最近写的代码,看看哪些函数是隐式非纯的(比如隐式依赖了
this或者全局配置)。 - 尝试重构: 尝试将一个修改传入对象的函数重构为返回新对象的纯函数。
- 体验 AI 协作: 尝试在你的 AI IDE 中选中一段纯函数和一段非纯函数,分别让 AI “解释这段代码”或“优化这段代码”,你会发现处理纯函数的效率和准确性要高得多。
理解这些概念,是你从“写代码”进阶到“设计架构”的重要一步。希望这篇文章能让你在编写下一行代码时,多一份对确定性的追求。