文章内容输出来源:拉勾教育 大前端高薪训练营
前言
学习函数式编程,要知道什么是纯函数,使用纯函数的好处,了解有关副作用的相关信息,以及纯函数相关的功能库Lodash,还有什么是函数的柯里化。
一、纯函数
1. 纯函数概念
一个函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用,我们就把这个函数叫做纯函数。
纯函数的特性相同的输入永远会得到相同的输出。函数的返回结果只依赖于它的参数。纯函数没有任何可观察的副作用。
相同的输入永远会得到相同的输出
纯函数就类似数学中的函数,函数将输入参数映射到返回值,也就是说,对于每套输入,都存在一个输出。
代码如下(示例):
const power= m => m * m;
纯函数没有任何可观察的副作用
一个函数执行过程对产生了外部可观察的变化,那么就说,这个函数是有副作用的。而 纯函数没有产生任何可观察的副作用,也就是说它不能改变任何外部状态。
代码如下(示例):
// 不纯的 let mini = 18 function checkAge (age) {return age >= mini }// 纯的(有硬编码,后续可以通过柯里化解决)function checkAge (age) {// 将 mini 变为局部变量,使外部程序观察不到,不会产生副作用let mini = 18 return age >= mini}
2. lodash功能库
lodash 是一个 JavaScript 实用工具库,提供一致性,及模块化、性能和配件等功能。它消除了处理数组的麻烦,从而简化了 JavaScript、 数字、对象、字符串等。
代码如下(示例):
// 演示 lodash// first / last / toUpper / reverse / each / includes / find / findIndexconst _ = 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)
详情参见 Lodash 中文网
3. 纯函数的好处
可缓存
因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来
代码如下(示例):
const _ = require('lodash') function getArea (r) {return Math.PI * r * r }let getAreaWithMemory = _.memoize(getArea) console.log(getAreaWithMemory(4))
自己模拟一个 memoize 函数
代码如下(示例):
function memoize (f) {let cache = {} return function () {let arg_str = JSON.stringify(arguments) cache[arg_str] = cache[arg_str] || f.apply(f, arguments) return cache[arg_str] } }
可测试
纯函数让测试更方便
并行处理
在多线程环境下并行操作共享的内存数据很可能会出现意外情况
纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数 (Web Worker)
4. 副作用
在上面我们讲述纯函数的特性时写到,纯函数没有任何可观察的副作用。那么,副作用的来源是什么,以及副作用将会导致什么样的影响呢?
副作用的来源
配置文件数据库获取用户的输入……所有的外部交互都有可能带来副作用,副作用也使得方法通用性下降不适合扩展和可重用性,同时副作用会给程序中带来安全隐患给程序带来不确定性,但是副作用不可能完全禁止,尽可能控制它们在可控范围内发生。
副作用的后果
副作用让一个函数变的不纯(如上例),纯函数的根据是相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用。
二、柯里化(Haskell Brooks Curry)
1. 柯里化(Currying)
维基百科上说道:柯里化(Currying),是把接受多个参数的函数变换成接受一个单一参数的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
也就是说,当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变),然后返回一个新的函数接收剩余的参数,返回结果,这就是函数的柯里化。
使用柯里化解决上一个案例中硬编码的问题
代码如下(示例):
// 柯里化 function checkAge (min) {return function (age) {return age >= min } }// ES6 写法 let checkAge = min => (age => age >= min)
2. lodash 中的柯里化函数
_.curry(func)功能:创建一个函数,该函数接收一个或多个 func 的参数,如果 func 所需要的参数都被提供则执行 func并返回执行的结果。否则继续返回该函数并等待接收剩余的参数。
参数:需要柯里化的函数。
返回值:柯里化后的函数。
代码如下(示例):
const _ = require('lodash') // 要柯里化的函数 function getSum (a, b, c) {return a + b + c }// 柯里化后的函数 let curried = _.curry(getSum) // 测试 curried(1, 2, 3) // 隐藏的柯里化函数:(a, b, c) => a + b + ccurried(1)(2)(3) // 隐藏的柯里化函数:a => b => c => a + b + ccurried(1, 2)(3) // 隐藏的柯里化函数:(a, b) => c => a + b + c
创建柯里化函数的通用方式
调用另一个参数并为它传入要柯里化的函数和必要参数。
代码如下(示例):
// carry()函数的主要工作就是将被返回函数的参数进行排序// 第一个参数是要进行柯里化的函数,其他参数是要传入的值function curry (fn) {// 获取第一个参数之后的所有参数// args包含了来自外部函数的参数var args = Array.prototype.slice.call(arguments, 1)return function () {// 存放所有传入的参数var innerArgs = Array.protptype.slice.call(arguments)var finalArgs = args.concat(innerArgs)// 使用 apply() 将结果传递给该函数return fn.apply(null, finalArgs)}}
模拟 _.curry() 的实现
代码如下(示例):
function curry (func) {// 获取函数的形参个数,可以通过 函数名.length // args 表示传进来的实际参数// 此处采取有名函数,而不是匿名函数,是因为当实参和形参的个数相同时,需要调用这个函数return function curriedFn (...args) {// ES6语法// 判断实参和形参的个数if (args.length < func.length) {// 当传入的参数 加上 剩余参数,等于形参个数时,执行下面的代码return function () {// 获取本次调用时,传入的参数return curriedFn(...args.concat(Array.from(arguments))) } }// 实参大于等于形参个数时,调用 func,返回结果 return func(...args) // ES6语法 ... 将数组展开} } // 要柯里化的函数 function getSum (a, b, c) {return a + b + c }let curried = curry(getSum) // 测试 // (args.length = 3) = func.length,直接调用func, 返回结果curried(1, 2, 3) // 1. (args.length = 1) < func.length , 执行匿名函数,调用curriedFn,传入参数2,此时 args.length = 2// 2. (args.length = 2) < func.length, 执行匿名函数,调用curriedFn,传入参数3,此时 args.length = 3// 3. (args.length = 3) = func.length,直接调用func, 返回结果curried(1)(2)(3) // 1. (args.length = 2) < func.length , 执行匿名函数,调用curriedFn,传入参数3,此时 args.length = 3// 2. (args.length = 3) = func.length,直接调用func, 返回结果curried(1, 2)(3)