纯函数
相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
纯函数就类似数学中的函数(用来描述输入和输出之间的关系),y = f(x)
举个例子:数组的 slice 和 splice 分别是:纯函数和不纯的函数
slice 返回数组中的指定部分,不会改变原数组
splice 对数组进行操作返回该数组,会改变原数组
// 纯函数和不纯的函数
// slice / splice
let array = [1, 2, 3, 4, 5]
// 纯函数
console.log(array.slice(0, 3))
console.log(array.slice(0, 3))
console.log(array.slice(0, 3))
// 不纯的函数
console.log(array.splice(0, 3))
console.log(array.splice(0, 3))
console.log(array.splice(0, 3))
// 纯函数
function getSum (n1, n2) {
return n1 + n2
}
console.log(getSum(1, 2))
console.log(getSum(1, 2))
console.log(getSum(1, 2))
函数式编程不会保留计算中间的结果,所以变量是不可变的(无状态的)。
我们可以把一个函数的执行结果交给另一个函数去处理。
接下来我们会用到lodash,lodash 是一个纯函数的功能库,提供了对数组、数字、对象、字符串、函数等操作的一些方法.(感兴趣的可以尝试如下方法,或者去看下官方文档)
// 演示 lodash
// first / last / toUpper / reverse / each / includes / find / findIndex
const _ = require('lodash')
const array = ['jack', 'tom', 'lucy', 'kate']
console.log(_.first(array))
console.log(_.last(array))
console.log(_.toUpper(_.first(array)))
console.log(_.reverse(array))
const r = _.each(array, (item, index) => {
console.log(item, index)
})
console.log(r)
// _.find
纯函数的好处
介绍了纯函数的概念,接下来说一下它的好处
1.可缓存。因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来。
如下代码会通过缓存存储中间输出过程,使得三次打印只执行一次。
// 记忆函数
const _ = require('lodash')
function getArea (r) {
console.log(r)
return Math.PI * r * r
}
let getAreaWithMemory = _.memoize(getArea)
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
接下来我们来自己模拟一下上述记忆函数,三次打印也会执行一次。
// 模拟 memoize 方法的实现
function memoize (f) {
let cache = {}
return function () {
let key = JSON.stringify(arguments)
cache[key] = cache[key] || f.apply(f, arguments)
return cache[key]
}
}
let getAreaWithMemory = memoize(getArea)
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
2.可测试。纯函数让测试更加方便
3.并行处理。纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数 (Web Worker)
副作用
副作用让一个函数变的不纯(如下例),纯函数的根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用
// 不纯的
let min=18
function checkAge (age) {
return age >= min
}
// 纯的(但是有硬编码,后续演示通过柯里化解决)
function checkAge (age) {
let min = 18
return age >= min
}
副作用的通常来源:
- 配置文件
- 数据库
- 用户的输入等
所有的外部交互都有可能带来副作用,副作用也使得方法通用性下降不适合扩展和可重用性,同时副作用会给程序中带来安全隐患给程序带来不确定性,但是副作用不可能完全禁止,尽可能控制它们在可控范围内发生
柯里化
当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变),然后返回一个新的函数接收剩余的参数,返回结果。
接下来使用柯里化来解决上一个案例中硬编码的问题:
// 存在硬编码的函数
function checkAge (age) {
let min = 18
return age >= min
}
// 普通的纯函数
function checkAge (min, age) {
return age >= min
}
console.log(checkAge(18, 20))
console.log(checkAge(18, 24))
console.log(checkAge(22, 24))
// 函数的柯里化
function checkAge (min) {
return function (age) {
return age >= min
}
}
// ES6
let checkAge = min => (age => age >= min)
let checkAge18 = checkAge(18)
let checkAge20 = checkAge(20)
console.log(checkAge18(20))
console.log(checkAge18(24))
在这里我将介绍ladash中的柯里化函数,并模拟柯里化的实现。
_.curry(func)
功能:创建一个函数,该函数接收一个或多个 func 的参数,如果 func 所需要的参数都被提
供则执行 func 并返回执行的结果。否则继续返回该函数并等待接收剩余的参数。
参数:需要柯里化的函数。
返回值:柯里化后的函数。
// lodash 中的 curry 基本使用
const _ = require('lodash')
function getSum (a, b, c) {
return a + b + c
}
const curried = _.curry(getSum)
console.log(curried(1, 2, 3))
console.log(curried(1)(2, 3))
console.log(curried(1, 2)(3))
案例:
// 柯里化案例
// ''.match(/\s+/g)
// ''.match(/\d+/g)
const _ = require('lodash')
const match = _.curry(function (reg, str) {
return str.match(reg)
})
const haveSpace = match(/\s+/g)
const haveNumber = match(/\d+/g)
const filter = _.curry(function (func, array) {
return array.filter(func)
})
const findSpace = filter(haveSpace)
// console.log(haveSpace('helloworld'))
// console.log(haveNumber('abc'))
console.log(filter(haveSpace, ['John Connor', 'John_Donne']))
console.log(findSpace(['John Connor', 'John_Donne']))
模拟_.curry()的实现:
function curry (func) {
return function curriedFn(...args) {
// 判断实参和形参的个数
if (args.length < func.length) {
return function () {
return curriedFn(...args.concat(Array.from(arguments)))
}
}
return func(...args)
}
}
总结:
- 柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数。
- 这是一种对函数参数的'缓存'。
- 让函数变的更灵活,让函数的粒度更小。
- 可以把多元函数转换成一元函数,可以组合使用函数产生强大的功能。
但是:柯里化很容易写出洋葱代码,有种像回调地狱般的嵌套感,后续我会介绍函数组合来解决这个不利于结构的问题。