函数式编程

211 阅读4分钟

函数式编程

函数式编程是一种编程范式,主要是利用函数把运算过程封装起来,通过组合各种函数来计算结果。

函数式编程的特点

  • 函数是“一等公民” 函数与其他数据类型一样,你可以像对待任何其他数据类型一样对待它们——把它们存在数组里,当作参数传递,赋值给变量…等等
// 函数作为参数传递
setTimeout(function() {
    console.log(1)
}, 1000)
  • 只用“表达式”,不用“语句”(声明式编程) "表达式"(expression)是一个单纯的运算过程,总是有返回值;"语句"(statement)是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。
  • 没有副作用 所谓"副作用",指的是函数内部与外部互动,产生运算以外的其他结果。 副作用包含:
    • 更改文件系统
    • 往数据库插入记录
    • 发送一个 http 请求
    • 可变数据
    • 打印/log
    • 获取用户输入
    • DOM 查询
    • 访问系统状态
  • 惰性执行 函数只在需要的时候执行,不产生无意义的中间变量。从头到尾都在写函数,只有在最后的时候才通过调用 产生实际的结果。 如Vue中路由的懒加载:
const List = () => import('@/components/list.vue') //只是定义了一个函数,没有执行import的动作
const router = new VueRouter({
  routes: [
    { path: '/list', component: List }
  ]
})
  • 引用透明 指的是函数的运行不依赖于外部变量或"状态",只依赖于输入的参数,任何时候只要参数相同,引用函数所得到的返回值总是相同的。
  • 无锁并发

柯里化

把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

优点: 函数柯里化之后让函数变得更加单一,一次只接受一个参数,松散解耦,提高了适用性。

缺点: 函数的通用性将变低,比如原来接收3个参数的函数,我们可以拿着3个参数处理更多操作,但是函数变为只接收一个参数后,我们的操作会受很多限制,降低了通用性。

主要使用场景:参数复用、延迟执行。

// 给数组arr中每个元素+1再+5再-2
let arr = [2, 5, 6, 3, 9];

// 正常做法
let arr1 = arr.map(val => {
    return val + 1
})

let arr2 = arr1.map(val => {
    return val + 5
})

let arr3 = arr2.map(val => {
    return val - 2
})
 console.log(arr3) // [6, 9, 10, 7, 13]

// 柯里化
 const calArr = (num) => {
    return (data) => {
        return data + num
    }
 }
// 返回的函数通过闭包即住了第一个传入的参数num
let arr4 = arr.map(calArr(1)).map(calArr(5)).map(calArr(-2))
console.log(arr4) // [6, 9, 10, 7, 13]

实现一个柯里化函数

函数柯里化的重点在于闭包和递归,将每次执行的作用域保存在内存中,等待后续使用

实现思路:

  1. 拿到原函数的形参个数length。
  2. 拿到目前接收到的参数args。
  3. 比较len和args大小。
  4. 根据大小判断返回一个函数还是返回原函数执行结果。
/**
  * @params {Function} fn 原函数
  * @params {Array} ...args 可以传入初始参数 
*/

function curry(fn, ...args) {
  // 返回一个函数
  return function () {
    // 缓存目前接收到的参数
    let _args = [...args, ...arguments];
    // 原函数形参个数
    let len = fn.length;
    // 比较目前的参数累计与原函数应该接收的参数
    if (_args.length < len) {
      // 代表需要继续返回一个新函数
      // 使用递归,形成闭包,保证函数独立,不受影响。
      return curry(fn, ..._args);
    } else {
      // 参数累计够了,执行原函数返回结果
      return fn(..._args);
    }
  }
}

// 一个普通函数
function sum(a, b, c) {
  return a + b + c;
}
// 正常调用
console.log(sum(10, 20, 30)); // 60


// 将sum函数柯里化,返回一个新函数
let newFn = curry(sum);
// 新的调用方式
console.log(newFn(10)(20)(30)); // 60

函数组合

函数组合就是将不同功能的函数组合成一个函数。 函数组合要满足结合律:改变函数组合顺序,效果一样 例如:

// 该三种组合模式都是等效的
let t = compose(f, g, h);
compose(compose(f, g), h) === compose(f, compose(g, h)); // true

常见的,使用lodash库来实现组合,

  • flow() 是从左到右运行
  • flowRight() 是从右到左运行
const _ = require('lodash');

const reverse = array => array.reverse();
const first = array => array[0];
const toUpper = str => str.toUpperCase();

const d = _.flowRight(toUpper, first, reverse);
const b = _.flow(reverse, first, toUpper);

console.log(d(['a', 'b', 'c'])); // C
console.log(b(['a', 'b', 'c'])); // C

###纯函数 纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。

  • 无副作用
  • 必须带参数
  • 纯函数不能调用非纯函数

###高阶函数 就是一个函数的参数是函数,或者返回值是函数,满足其中一个就是高阶函数 常用的高阶函数:

  1. 数组的高阶用法
  • forEach()
  • filter()
  • find/findIndex()
  • map()
  • some()
  • every()
  • sort()
  • reduce()
  1. 防抖节流函数 ###函子 参考:阮一峰函数式编程入门教程

函子就是一个特殊的容器,它可以由对象来实现,这个对象中包含了值,这个值永远不会对外公布,有一个map方法,用来操作这个值。还有一个of方法,用来生成一个新的容器。

class Functor {
  static of(value){
    return new Functor(value)
  }

  constructor(val){
    this._value = val
  }

  map(fn){
    return Functor.of(fn(this._value ))
  }
}

//使用的时候
const func = Functor.of.map(x => x+1)
func(1)//Container { _value: 2 }