Javascript中的函数式编程

208 阅读4分钟

函数式编程就是使用一些可复用的函数组合来解决问题, 在Javascript中函数是"一等公民"可以出现在任何地方: 可以其他数据类型一样赋值给变量、可以作为函数参数、可以是函数返回值;

纯函数

所谓纯函数,就一句话概括: 相同的输入,一定会产生相同的输出

  1. 函数内不使用Math.random(), new Data()等随机不确定的值

    这个好理解, 因为函数内不确定的随机数,可能会导致返回的结果不一致

  2. 不使用全局的状态

    因为全局的状态可能再外部被别的程序修改导致返回结果变化

    let number = 10
    function compare(value) {
    	return value > number // 如果外部修改了全局变量number的值,返回的结果就不一定了
    }
    
  3. 函数中的参数不能变化

高阶函数

function fn() {
    return "Hello world"
  }

//接收一个函数作为参数的函数
function greet(fn) { // 更推荐 const greet = fn; greet()这种写法
  console.log(fn())
}
//以函数作为返回值的函数
function sayHello() {
  return fn
}

像上面两个函数greetsayHello就都是高阶函数,接收一个函数作为参数或以函数作为返回值的情况都属于高阶函数。其实我们平时也经常用到高阶函数, 只是没有主意而已。

数组的一些方法mapfilterreduce, 定时器setTimeoutsetInterval等都接收一个函数作为参数

[1,2,3].map(item => item*2) 
setTimeout(fn, 1000) //函数fn

函数复合

一个功能可能是有多个简单函数、按照一定的执行顺序组合完成的; 一个简单的生成DOM元素的例子:将一个全小写的、去除两边空格的字符串放入 div标签里。

  1. 去除字符串两边的空格

    function trim(str) { return str.trim() }
    
  2. 将字符串转为小写

    function toLowerCase(str) { return str.toLowerCase() }
    
  3. 将字符串放入div标签

    function wrapInDiv(str) { return `<div>${str}</div>` }
    
  4. 按照一定的顺序依次调用

    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就可以看成是函数对象的属性, 返回函数形参个数

  1. 返回的是形参个数, 而不是实际参数个数(arguments.length)

  2. 如果函数参数具有默认值, 则返回从左到右第一个具有默认值参数位置之前的参数个数

    (function(a=0, b, c){}).length // 0
    (function(a, b=0, c){}).length  // 1
    (function(a, b, c=0){}).length // 2
    
  3. ES6中的rest参数不计入

    (function(a, b, ...rest){}).length // 2 rest参数接收函数剩余参数,必须放在最后一位