纯函数
- 纯函数:相同的输入永远会得到相同的输出,而且没有任何可观察 的副作用
- 类比:纯函数就类似数学中的函数(用来描述输入和输出之间的关系,也可以称作为映射,如:y = f(x))
如下图所示:左侧代表输入,右侧代表输出,f标识输入和输出之间的关系
- lodash是一个纯函数的功能库,提供了对数组、数字、字符串、函数等操作的一些方法
- 其中数组的slice是纯函数,splice不是纯函数
- slice 返回数组中的指定部分,不会改变原数组
- splice 对数组进行操作后返回数组,会改变原数组
slice和splice示例:
// slice
let nums = [1, 2, 3, 4, 5]
// 多次调用slice(start, end) 不包括end位
nums.slice(0, 3) // => [1, 2, 3]
nums.slice(0, 3) // => [1, 2, 3]
nums.slice(0, 3) // => [1, 2, 3]
// 在多次调用时相同的输入得到相同的输出
// splice
let nums = [1, 2, 3, 4, 5]
// 多次调用slice(start, length)
nums.splice(0, 3) // => [1, 2, 3]
nums.splice(0, 3) // => [4, 5]
nums.splice(0, 3) // => []
// 在多次调用时相同的输入得到不同的输出
- 函数式编程不会保留计算的中间结果,所以变量是不可变的(无状态的)
- 我们可以把一个函数的执行结果交给另一个函数去处理
纯函数的好处
- 可缓存
- 纯函数对相同的输入始终有相同的输出,所以可以把纯函数的结果缓存起来
- 可测试
- 纯函数让测试更方便(输入对应输出,单元测试就是断言输出结果)
- 并行处理
- 在多线程环境下并行操作共享内存数据很可能会出现意外情况(如:同时修改全局变量)
- 纯函数不需要访问共享的数据内存,所以在并行环境下可以任意运行纯函数(ES6新增Web Worker可以开启多线程)
示例:lodash中的记忆函数memoize
// 求圆的面积
const _ = require('lodash')
// 创建求圆面积的纯函数
function getArea(r) {
// 为了验证该函数被执行几次
console.log(r)
return Math.PI * r * r
}
// memoize会返回一个带记忆功能的函数
let getAreaWithMemory = _.memoize(getArea)
// 执行多次getAreaWithMemory
console.log(getAreaWithMemory(2))
console.log(getAreaWithMemory(2))
console.log(getAreaWithMemory(2))
// 输出结果
// 2
// 12.566368
// 12.566368
// 12.566368
// 此时我们发现getArea函数内部的log只输出了1次
示例:模拟lodash中memoize函数实现
// memoize接收一个函数,返回值也是一个函数
function memoize(fn) {
// 定义一个对象把函数的执行结果缓存起来
// fn为一个纯函数,相同的输入对应相同的输出
// 因此对象中key为输入数据,value为函数执行结果
let cache = {}
return function() {
// 执行函数时首先判断cache中是否已缓存
// 已缓存:不再去计算直接从cahce中取
// 未缓存:计算后将结果缓存起来
let key = JSON.stringify(arguments) // 将伪数组arguments转换成string
// 判断cache中是否已有缓存
// apply方法可以改变函数内部this,也可以将参数展开
cache[key] = cache[key] || fn.apply(fn, arguments)
return cache[key]
}
}
// 测试
let getAreaWithMemory = memoize(getArea)
// 执行多次getAreaWithMemory
console.log(getAreaWithMemory(2))
console.log(getAreaWithMemory(2))
console.log(getAreaWithMemory(2))
纯函数的副作用
- 纯函数:对于相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
示例:年龄判断
// 非纯函数
let mini = 18 // mini为全局变量,当发生变化时不满足纯函数第一条
function checkAge(age) {
return age >= mini
}
// 纯函数(有硬编码,后边可通过函数的柯里化解决)
function checkAge(age) {
let mini = 18 // 编成局部变量
return age >= mini
}
- 副作用:让一个函数变得不纯(相同输入不能保证相同输出),如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用
- 副作用来源:
- 全局变量
- 配置文件
- 数据库
- 获取用户的输入
- ......
所有的外部交互都有可能带来副作用,副作用也使得方法的通用性降低不适合扩展和可重用性,同事副作用会给程序带来安全隐患(跨站脚本攻击),但是副作用不可能完全禁止,尽可能控制他们在可控范围内发生