纯函数是函数式编程的基本组成。因为其简单、可测试而被推崇。 本文提供了快速检查表来判断一个函数是否为纯函数。
检查表
函数必须通过两条检测才能被认为“纯”:
1.相同的输入总返回相同的输出
2.无副作用
让我们扩展开来看看。
1.相同输入=>相同输出
对比这段:
const add = (x, y) => x + y;
add(2, 4); // 6
和这段:
let x = 2;
const add = (y) => {
x += y;
};
add(4); // x === 6 (the first time)
纯函数 = 一致的结果
第一个例子根据赋值参数返回一个值,不管你在何时何地调用它。
例如你传入2和4,你总会得到6。
没有其他东西影响该输出。
非纯函数 = 不一致性的结果
第二个例子无返回值。它依赖共享状态完成它的功能,通过在它作用域外部增加一个变量。
这种模式是开发者的噩梦。
共享状态带来时间依赖。你会获得不同的结果,依据你何时调用该方法。第一次你得的结果是6,下一次是10,以此类推。
哪个版本更容易推断呢?
哪个更不容易滋生那些只在特定条件下才发生的bug?
哪个会更容易在多线程环境下成功,该环境下时间依赖可能破坏系统?
显然是第一个。
2.无副作用
这个测试本身就是一份检查表,有几个有副作用的例子:
- 修改你的输入
- console.log
- HTTP请求(ajax/fetch)
- 修改文件系统(fs)
- 查询DOM
基本上一个函数做的任何与其计算最终输出无关的事情都算。
这是一个有副作用的非纯函数。
没那么糟糕的
const impureDouble = (x) => {
console.log('doubling', x);
return x * 2;
};
const result = impureDouble(4);
console.log({ result });
console.log 在这里是一个副作用,大部分情况下,它无害。在赋相同值时,我们也会得到一样的输出。然而,以下可能会有问题:
污染性的更改对象
const impureAssoc = (key, value, object) => {
object[key] = value;
};
const person = {
name: 'Bobo'
};
const result = impureAssoc('shoeSize', 400, person);
console.log({
person,
result
});
变量person,被永久性的更改掉了,因为我们的函数加了赋值声明。
共享状态意味着impureAssoc的作用不再完全明确。现在也理解他对系统的影响,就涉及跟踪他接触过的每一个变量,并知道他们的历史。
共享状态 = 时间依赖
我们可以通过简单的返回一个包含我们想要的属性的新对象来纯化impureAssoc。
纯化
const pureAssoc = (key, value, object) => ({
...object,
[key]: value
});
const person = {
name: 'Bobo'
};
const result = pureAssoc('shoeSize', 400, person);
console.log({
person,
result
});
目前 pureAssoc返回了可测试的结果,我们不再担心它悄悄的在其他地方修改了什么。
你也可以像下面这样,保持其纯净:
const pureAssoc = (key, value, object) => {
const newObject = { ...object };
newObject[key] = value;
return newObject;
};
const person = {
name: 'Bobo'
};
const result = pureAssoc('shoeSize', 400, person);
console.log({
person,
result
});
更改你的输入是危险的行为,然而修改它的拷贝却不会有问题。我们最终的结果仍然是可测试、可预测的函数。无论你何时何地调用它,它都会正常工作。
修改被限制在那很小的范围,而你也仍然会返回一个值。
对象深度拷贝
当心!使用扩展运算符...会创建对象的浅拷贝。在嵌套修改中浅拷贝并不安全。
不安全的嵌套修改
const person = {
name: 'Bobo',
address: { street: 'Main Street', number: 123 }
};
const shallowPersonClone = { ...person };
shallowPersonClone.address.number = 456;
console.log({ person, shallowPersonClone });
person 和 shallowPersonClone 都被修改掉了,因为他们的子元素共享了相同的引用。
安全的嵌套修改
我们需要深度拷贝来进行安全地修改嵌套属性。
const person = {
name: 'Bobo',
address: { street: 'Main Street', number: 123 }
};
const deepPersonClone = JSON.parse(JSON.stringify(person));
deepPersonClone.address.number = 456;
console.log({ person, deepPersonClone });
这下你保证安全了,因为他们实际上是两个实例。
总结
- 如果一个函数没有副作用,当输入相同的值会输出相同的返回值那么它是纯函数。
- 副作用包括:修改输入,HTTP请求,写磁盘,打印到显示屏。
- 对于输入,你可以安全的拷贝,并修改它。只要别碰原始值。
- 扩展运算语法(...syntax)是对象浅拷贝最简单的方式。
- JSON.parse(JSON.stringify(object))是对象深拷贝最简单的方式。
关于本文
译者:Sandy Liang
作者:Yazeed Bzadough