函数式编程就是使用一些可复用的函数组合来解决问题, 在Javascript中函数是"一等公民"可以出现在任何地方: 可以其他数据类型一样赋值给变量、可以作为函数参数、可以是函数返回值;
纯函数
所谓纯函数,就一句话概括: 相同的输入,一定会产生相同的输出
-
函数内不使用Math.random(), new Data()等随机不确定的值
这个好理解, 因为函数内不确定的随机数,可能会导致返回的结果不一致
-
不使用全局的状态
因为全局的状态可能再外部被别的程序修改导致返回结果变化
let number = 10 function compare(value) { return value > number // 如果外部修改了全局变量number的值,返回的结果就不一定了 }
-
函数中的参数不能变化
高阶函数
function fn() {
return "Hello world"
}
//接收一个函数作为参数的函数
function greet(fn) { // 更推荐 const greet = fn; greet()这种写法
console.log(fn())
}
//以函数作为返回值的函数
function sayHello() {
return fn
}
像上面两个函数greet
、sayHello
就都是高阶函数,接收一个函数作为参数或以函数作为返回值的情况都属于高阶函数。其实我们平时也经常用到高阶函数, 只是没有主意而已。
数组的一些方法map
、filter
、reduce
, 定时器setTimeout
、setInterval
等都接收一个函数作为参数
[1,2,3].map(item => item*2)
setTimeout(fn, 1000) //函数fn
函数复合
一个功能可能是有多个简单函数、按照一定的执行顺序组合完成的; 一个简单的生成DOM元素的例子:将一个全小写的、去除两边空格的字符串放入 div
标签里。
-
去除字符串两边的空格
function trim(str) { return str.trim() }
-
将字符串转为小写
function toLowerCase(str) { return str.toLowerCase() }
-
将字符串放入div标签
function wrapInDiv(str) { return `<div>${str}</div>` }
-
按照一定的顺序依次调用
var result = wrapInDiv(toLowerCase(trim(" JavaScript ")))
函数由内向外依次调用,将结果作为下一个函数的参数;虽然功能实现了, 但是这种函数必须从右往左运算,层层传递调用看的很费劲啊,随着函数的复杂性增加,这些括号会越来越多。 有没有什么办法解决呢???如果我们有一个函数,可以接收多个函数作为参数,并按照参数的传入顺序依次调用执行不就可以了吗。 这就是函数复合compose想要做的事。
function compose(...funs) {
const length = funs.length
if(length === 0) {
return arg => arg //需要返回一个空函数
} else if(length === 1) {
return funs[0]
} else {
return funs.reduce( (a, c) => (...args) => c(a(...args)) )
}
}
上面的compose
函数也不能理解,就是最后一行代码理解比较费劲,别急 下面来详细解释一下。
首先数组的reduce
方法我们不陌生,就是对数组中的每个元素执行一个由我们传入的reducer函数,并将结果汇总为单个值返回。
[1,2,3,4,5].reduce((accumulator, currentValue) => {
console.log(accumulator, currentValue);
return accumulator + currentValue;
} )
// 1 2
// 3 3
// 6 4
// 10 5
// 15
再则compose()
的返回值本身就是一个函数, 所以reduce也需要返回一个函数
funs.reduce(function(a, c) {
//最内层函数在调用时需要传入的参数在这里提供,其他外层函数的参数都是由上一层函数的返回值提供
return function(...args) {
c(a(...args)) //为了使函数按照传参的顺序从左到右依次调用
}
})
//改成箭头函数形式
funs.reduce((a, c) => (...args) => c(a(...args)))
函数柯里化
接着上面的例子继续,上面的函数只能生成固定元素的标签,如何实现同字符串一样通过传参动态生成呢?
function wrap(element, str) { return `<{${element}}>${str}</${element}>` }
再通过函数复合调用
const result = compose(trim, toLowerCase, wrap)
result("Functional") // <functional>undefined</functional>
直接这样肯定是实现不了的, 因为我们没有给wrap
函数传递第二个参数, 所以才导致函数的第一个参数element
变成了functional
, 第二个参数由于没有传值变成了undefined
,但是compose
函数要求参数类型都必须是函数,这里咋为第三个参数wrap
函数传值呢? 而且wrap
函数在经复合函数compose
执行后,会接收上个函数toLowerCase
的返回值作为参数。
函数柯里化: 将一个接收多个参数的函数转换成接收一个参数的函数,并返回一个函数处理余下的参数
例如这个求两数字之和的函数
function add(x,y) {
return x + y
}
函数柯里化之后:
function add(x) {
return function (y) {
return x + y
}
}
//转成箭头函数形式
const add = x => y => x + y
将函数柯里化之后,函数add
现在接收一个参数并返回一个新函数, add()
调用之后返回的这个新函数通过闭包的方式获取传入add
函数的参数;
在继续回到上面动态生成元素类型的问题,我们将wrap函数柯里化
//原函数
const wrap = (element, str) => `<${element}>${str}</${element}>`
//柯里化之后
const wrap = element => str => `<${element}>${str}</${element}>`
再通过复合函数调用
const result = compose(trim, toLowerCase, wrap('div'))
result("Functional") // <div>Functional</div>
函数柯里化通用函数
function curry(fn, args=[]) { //定义局部变量args,后面通过闭包的形式获取该值
//函数经柯里化转化后,最后生成的还是一个函数
return function() {
//变量收集,将之前传入的变量与当前传入的变量合并
args = args.concat([].slice.call(arguments))
//定义递归入口和出口, 当收集的变量个数等于目标函数所需的参数个数时,结束递归并调用目标函数
if(args.length < fn.length) { //fn.length 可以直接获取函数参数个数
return curry(fn, args)
}
else {
//将收集的参数传入目标函数, 也可以换用 fn.call(undefined, ...args)
return fn.apply(undefined, args)
}
}
}
这应该是最简单的柯里化通用函数了,主要就是不断的通过递归收集传入函数的参数, 当收集的函数参数个数等于所需的原函数参数时,将参数传入原函数并调用。
补充:函数的length
属性
函数可以看成是Function
构造函数的实例,那么length
就可以看成是函数对象的属性, 返回函数形参个数
-
返回的是形参个数, 而不是实际参数个数(arguments.length)
-
如果函数参数具有默认值, 则返回从左到右第一个具有默认值参数位置之前的参数个数
(function(a=0, b, c){}).length // 0 (function(a, b=0, c){}).length // 1 (function(a, b, c=0){}).length // 2
-
ES6中的rest参数不计入
(function(a, b, ...rest){}).length // 2 rest参数接收函数剩余参数,必须放在最后一位