各位观众老爷,大家好!今天咱们聊聊JavaScript里的“纯函数”这玩意儿。别看它名字听着像高冷女神,其实是个务实居家型的好伙伴,能让你的代码变得更靠谱、更易懂、更方便“调戏”(测试)。
开场白:为什么要搞“纯”?
想象一下,你辛辛苦苦写了一段代码,结果每次运行出来的结果都不一样,就像抽奖一样,全凭运气。这酸爽,谁用谁知道!这就是“非纯函数”的威力,它们会偷偷摸摸地修改外部世界,让你防不胜防。
而“纯函数”就不一样了,它们就像老实人,只干自己分内的事,不搞小动作,给你稳定的预期。用人话说,就是:
- 相同的输入,永远得到相同的输出。
- 没有任何副作用。
啥叫“副作用”?
“副作用”就是指函数在执行过程中,除了返回结果之外,还对外部环境产生了影响。比如:
- 修改了全局变量
- 修改了函数参数
- 进行了 I/O 操作(比如读写文件、网络请求)
- 操作了 DOM
这些行为都可能导致你的代码变得难以预测和调试。
纯函数的好处:简直不要太多!
- 可测试性高: 因为输入和输出是确定的,所以你可以很容易地编写单元测试来验证函数的正确性。
- 可维护性强: 函数之间相互独立,修改一个函数不会影响到其他函数,降低了维护成本。
- 可预测性好: 相同的输入总是得到相同的输出,更容易理解和调试代码。
- 易于复用: 纯函数只依赖于输入参数,可以在不同的场景下复用。
- 并发安全: 由于没有副作用,纯函数可以在并发环境中安全地执行。
- 方便缓存: 因为结果只依赖于输入,所以可以对纯函数的结果进行缓存,提高性能(这就是所谓的“memoization”)。
纯函数 VS 非纯函数:举个栗子
我们先来个简单的例子,对比一下纯函数和非纯函数的区别:
// 非纯函数:修改了全局变量
let counter = 0;
function incrementCounter() {
counter++;
return counter;
}
console.log(incrementCounter()); // 输出 1
console.log(incrementCounter()); // 输出 2
console.log(counter); // 输出 2
// 纯函数:不修改外部状态
function add(a, b) {
return a + b;
}
console.log(add(2, 3)); // 输出 5
console.log(add(2, 3)); // 输出 5
console.log(counter); // 输出 2 (counter 的值没有改变)
在这个例子中,incrementCounter
是一个非纯函数,因为它修改了全局变量 counter
。每次调用 incrementCounter
,counter
的值都会增加,导致输出结果不确定。而 add
是一个纯函数,它只依赖于输入参数 a
和 b
,并且没有副作用,所以每次调用 add(2, 3)
都会得到相同的结果 5
。
再来点复杂的:数组操作
数组操作是 JavaScript 中常见的任务,我们来看看如何用纯函数的方式来处理数组:
// 非纯函数:修改了原数组
function addItem(arr, item) {
arr.push(item);
return arr;
}
let myArray = [1, 2, 3];
let newArray = addItem(myArray, 4);
console.log(myArray); // 输出 [1, 2, 3, 4] (原数组被修改了)
console.log(newArray); // 输出 [1, 2, 3, 4]
// 纯函数:不修改原数组,返回一个新数组
function addItemPure(arr, item) {
return [...arr, item]; // 使用扩展运算符创建新数组
}
let myArrayPure = [1, 2, 3];
let newArrayPure = addItemPure(myArrayPure, 4);
console.log(myArrayPure); // 输出 [1, 2, 3] (原数组没有被修改)
console.log(newArrayPure); // 输出 [1, 2, 3, 4]
addItem
直接修改了原数组 myArray
,这是副作用。而 addItemPure
使用了扩展运算符 ...
创建了一个新的数组,并将 item
添加到新数组中,原数组 myArrayPure
没有被修改,这才是纯函数该有的样子。
常用的纯函数技巧:
- 使用
map
、filter
、reduce
等数组方法: 这些方法都是纯函数,它们不会修改原数组,而是返回一个新的数组。 - 使用扩展运算符
...
创建新数组或新对象: 避免直接修改原有的数据结构。 - 避免修改函数参数: 将函数参数视为只读的。
- 尽量使用局部变量: 避免使用全局变量,减少副作用。
表格总结:纯函数 vs 非纯函数
特性 | 纯函数 | 非纯函数 |
---|---|---|
输入 | 相同的输入 | 相同的输入 |
输出 | 永远得到相同的输出 | 可能得到不同的输出 |
副作用 | 没有副作用 | 存在副作用 |
可测试性 | 高 | 低 |
可维护性 | 强 | 弱 |
可预测性 | 好 | 差 |
并发安全 | 安全 | 不安全 |
缓存 | 方便缓存 (memoization) | 难以缓存 |
例子 | add(a, b) => a + b |
incrementCounter() { counter++; } |
数组操作 | [...arr, item] (创建新数组) |
arr.push(item) (修改原数组) |
代码示例:一个稍微复杂点的例子(购物车计算)
假设我们要计算一个购物车的总价,可以这样写:
// 购物车数据 (商品ID, 价格, 数量)
const cart = [
{ id: 1, price: 10, quantity: 2 },
{ id: 2, price: 20, quantity: 1 },
{ id: 3, price: 5, quantity: 4 },
];
// 纯函数:计算单个商品的总价
function calculateItemTotal(item) {
return item.price * item.quantity;
}
// 纯函数:计算购物车总价
function calculateCartTotal(cart) {
return cart.reduce((total, item) => total + calculateItemTotal(item), 0);
}
const total = calculateCartTotal(cart);
console.log("购物车总价:", total); // 输出 购物车总价: 60
在这个例子中,calculateItemTotal
和 calculateCartTotal
都是纯函数。它们只依赖于输入参数,并且没有副作用,可以很容易地进行测试和复用。
关于副作用的“控制”
虽然我们推崇纯函数,但这并不意味着要完全消除副作用。在实际开发中,有些操作是不可避免的,比如 I/O 操作、DOM 操作等。关键在于我们要控制副作用,尽量将它们隔离在特定的模块或函数中,使其影响范围最小化。
例如,可以将 UI 渲染操作放在一个单独的函数中,这个函数可能不是纯函数,但它可以将副作用限制在 UI 层面,而不会影响到其他模块的逻辑。
函数式编程:纯函数的最佳搭档
纯函数是函数式编程的核心概念之一。函数式编程强调使用纯函数来构建应用程序,通过组合纯函数来实现复杂的功能。函数式编程可以提高代码的可读性、可维护性和可测试性,使代码更加健壮。
一些小提示:
- 代码审查: 在代码审查过程中,重点关注函数是否是纯函数,是否有副作用。
- 重构: 如果发现代码中存在非纯函数,尝试将其重构为纯函数。
- 工具: 可以使用一些工具来辅助检查代码中的副作用,比如 ESLint。
最后总结
纯函数是 JavaScript 中一种非常有用的编程技巧,它可以提高代码的可测试性、可维护性和可预测性。虽然不能完全避免副作用,但我们可以通过控制副作用,尽量使用纯函数来构建应用程序。拥抱纯函数,让你的代码更上一层楼!
感谢各位的观看,希望今天的分享对大家有所帮助!下次有机会再和大家聊聊其他有趣的编程话题。