基本概念
在程序设计中,若一个函数符合以下要求,则它可能被认为是纯函数(Pure Function):
- 相同的输入值时,需产生相同的输出
- 函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关
- 不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等
相同的输入值时,需产生相同的输出
纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,否则就不是纯函数。
例如,下面例子,无论上下文是什么,无论调用多少次double(2)返回值永远是4,可以直接用于替换double(2);而random(2)每次调用返回一个0~n之间的随机数,返回值每次都可能发生改变。所以,double是纯函数,而random不是。
function double(n){
return n * 2
}
double(2) // 4
double(2) // 4
function random(n){
return n * Math.random()
}
random(2) // 0.3131058434716416
random(2) // 0.46288851095052275
函数的输出和输入值以外的其他隐藏信息或状态无关
纯函数的输出可以不用和所有的输入值有关,甚至可以和所有的输入值都无关。但纯函数的输出不能和输入值以外的任何元素有关。比如,函数依赖一个全局变量,或者作用域以外的变量,或者一个dom元素的属性或者内容,这些信息随时可能会发生改变,从而影响到函数的输出。
下面double函数中的i是外部变量,随时可能被修改,进而影响到double函数的输出,所以这里double不是纯函数。getInfo()输出取决dom的内容信息,也不是纯函数。
var i = 2
function double(n){
return n * 2
}
function getInfo(){
return `get info from element:${$('#info').text()}`
}
我们可以保证外部元素信息是不可变的,从而保证函数的输入和输出的一致性。只要保证函数不受外部因素影响、相同的输入值,其输出值也相同,且对外不产生副作用,就是纯函数。但是,javascript中保证变量不可变,是很困难的。
- 不可变性的讨论
const关键字声明的number、boolean、string类型的常量是不可变的。这里的double是纯函数
const i = 2
function double(n){
return n * i
}
double(2) // 4
i = 3 // TypeError: Assignment to constant variable
double(2) // 4
我们无法保证const声明的object类型的数据不可变。这里的double不是纯函数
const o = { i: 2 }
function double(n){
return n * o.i
}
double(2) // 4
o.i = 3
double(2) // 6
Object.freeze可以冻结对象,保证其不可变。下面的double是纯函数
var o = Object.freeze({i:2})
function double(n){
return n * o.i
}
double(2) // 4
o.i = 3
double(2) // 4
Object.freeze无法保证对象深层次的属性不可变
var o = Object.freeze({k:{i:2}})
function double(n){
return n * o.k.i
}
double(2) // 4
o.k.i = 3
double(2) // 6
deepFreeze可以保证object的不可变
const deepFreeze = (obj) => {
for (let prop of Object.keys(obj)) {
if (typeof obj[prop] === 'object') {
Object.freeze(obj[prop]);
deepFreeze(obj[prop]);
}
}
}
不能有语义上可观察的函数副作用
纯函数不应该对函数外部元素产生副作用。如果执行过程中修改和影响了外部状态,那么就不是纯函数。比如,触发事件、使用输入输出设备、修改外部变量、修改dom属性或者内容、发送一个信息到服务端、保存和修改文件信息等,诸如此类的副作用还有很多。
下面实例中,log函数向控制台打印信息,也被认为是副作用。
function log(info){
console.log(info)
}
下面实例,change函数虽然修改的是输入的参数值,但是同时改变了外部变量obj,也是副作用。
var obj = {
name: 'zhangsan'
}
function change(o){
o.name = 'lisi'
}
change(o)
数组的slice和splice方法,具备相似的功能,性质区别却很大。slice没有副作用,但是splice却是有副作用的。
let arr = ['a','b','c']
let arr1 = arr.slice(1,2)
// arr1 === ['b']
// arr === ['a','b','c']
let arr2 = arr.splice(1,1)
// arr2 === ['b','c']
// arr === ['a']
纯函数的优点和用途
没有副作用,利于重构
纯函数没有副作用,不会影响函数外部元素的状态。可以放心的对纯函数进行重构,而不必担心会对其他模块产生任何影响。
不受外部状态影响,利于并行处理
纯函数完全独立于外部状态,因此,它们不受所有与共享可变状态有关问题的影响。它们的独立特性也使其成为跨多个 CPU 以及整个分布式计算集群进行并行处理的极佳选择,这使其对许多类型的科学和资源密集型计算任务至关重要。
相同的输入,输出永远固定,利于测试,利于缓存
纯函数确保输入和输出的一致性,对于测试至关重要。我们不需要伪造一个“真实的”环境,或者每一次测试之前都要配置、之后都要断言状态(assert the state)。只需简单地给函数一个输入,然后断言输出就好了。
let greeting = 'Hello'
// 依赖外部变量greeting,非纯函数
function greet1 (name) {
return greeting + ' ' + name
}
// 纯函数
function greet2 (greeting, name) {
return greeting + ' ' + name
}
// 无法保证测试的正确性
describe('greet1', function() {
it('shows a greeting', function() {
expect(greet1('Savo')).toEqual('Hello Savo')
})
})
// 可以保证测试的正确性
describe('greet2', function() {
it('shows a greeting', function() {
expect(greet2('Hello','Savo')).toEqual('Hello Savo')
})
})
基于纯函数的输入与输出的一致性,可以上一次执行的结果缓存,以供后面使用
/**
* fn是纯函数
*/
export function cached(fn) {
const cache = Object.create(null)
return function cachedFn(str) {
const hit = cache[str]
return hit || (cache[str] = fn(str))
}
}
/**
* 实例
*/
export const capitalize = cached((str) => {
return str.charAt(0).toUpperCase() + str.slice(1)
})
// 第一次调用,缓存cache['hello world'] = fn('hello world')
capitalize('hello world') // Hello world
// 第二次调用直接从cache['hello world']中取出,不会经历fn执行
capitalize('hello world') // Hello world
参考资料
zh.wikipedia.org/wiki/%E7%BA… llh911001.gitbooks.io/mostly-adeq… juejin.cn/post/684490… zhuanlan.zhihu.com/p/121627485 levelup.gitconnected.com/functional-…