检查清单
纯函数是函数式编程中的基本构建单元,因其简洁性和可测试性而备受推崇。然而,并非所有函数都是纯的,本节将帮助你自行判断一个函数是否为纯函数。(5分钟阅读)
纯函数
一个函数要被称为“纯函数”,必须通过两个测试:
- 相同的输入始终返回相同的输出
- 没有副作用
Ramda 遵循这些规则,以在 JavaScript 中推动函数式编程。下面我们逐一深入分析。
相同输入 → 相同输出
对比以下两个例子:
1const add = (x, y) => x + y; 2 3add(2, 4); // 6
与这个:
1let x = 2; 2 3const add = (y) => { 4 x += y; 5}; 6 7add(4); // x === 6(第一次调用时)
第一个例子无论何时调用,只要传入 2 和 4,结果都是 6。而第二个例子会修改外部变量 x。
这种共享状态引入了时间依赖性——函数的结果取决于调用的时间点。第一次调用得 6,第二次得 10,依此类推。
哪个版本更容易理解?哪个更不容易产生仅在特定条件下出现的 bug?哪个在多线程环境中更可靠(因为时间依赖可能破坏系统)?毫无疑问是第一个。
无副作用
这本身就是一个检查清单。常见的副作用包括:
- 修改输入参数
- 使用
console.log - HTTP 请求(如 AJAX/fetch)
- 修改文件系统
- 操作 DOM
本质上,凡是与函数最终输出无关的“额外操作”都属于副作用。违反这一原则可能导致我们之前讨论过的状态管理问题。
下面是一个带有副作用的不纯函数示例:
1const impureDouble = (x) => { 2 console.log('正在加倍', x); 3 4 return x * 2; 5}; 6 7const result = impureDouble(4); 8 9console.log({ result });
虽然这个副作用在实际使用中几乎不会造成危害,因为我们仍能保证相同输入得到相同输出。
但以下情况可能会引发问题:
1const impureAssoc = (key, value, object) => { 2 object[key] = value; 3}; 4 5const person = { name: 'Bobo' }; 6const result = impureAssoc('shoeSize', 400, person); 7 8console.log({ person, result });
变量 person 已被永久修改,因为函数内部执行了赋值操作。
共享状态意味着 impureAssoc 的影响不再显而易见。要理解它对系统的影响,必须追踪所有被它修改过的变量及其历史记录。
共享状态 = 时间依赖。
我们可以将 impureAssoc 改为纯函数,只需返回一个包含所需属性的新对象:
1const pureAssoc = (key, value, object) => ({ 2 ...object, 3 [key]: value 4}); 5 6const person = { name: 'Bobo' }; 7const result = pureAssoc('shoeSize', 400, person); 8 9console.log({ person, result });
现在 pureAssoc 返回的是可测试的结果,我们无需担心它悄悄地改变了其他地方的数据。
你甚至可以这样写,依然保持纯性:
1const pureAssoc = (key, value, object) => { 2 const newObject = { ...object }; 3 4 newObject[key] = value; 5 6 return newObject; 7}; 8 9const person = { name: 'Bobo' }; 10const result = pureAssoc('shoeSize', 400, person); 11 12console.log({ person, result });
修改输入本身是有风险的,但修改其副本则完全安全。最终结果仍然是一个可测试、可预测的函数,无论在哪里或何时调用都表现一致。修改仅限于局部作用域,且仍返回可测试的结果。
总结
-
函数是纯的,当且仅当它没有副作用,并且给定相同输入总是返回相同输出。
-
副作用包括:修改输入、HTTP 请求、写入磁盘、向屏幕输出内容。
-
可以安全地修改输入的副本,原始数据应保持不变。
-
展开语法(spread syntax)是克隆对象和数组最简便的方式。
1const person = { name: 'Bobo' }; 2const personClone = { ...person }; 3 4const people = ['Bobo', 'Barry', 'Bruce']; 5const peopleClone = [...people]; 6 7personClone.name = 'Sam'; 8peopleClone[0] = 'Sam'; 9 10console.log({ 11 person, 12 personClone, 13 people, 14 peopleClone 15});