JavaScript高级深入浅出:函数式编程

648 阅读6分钟

介绍

本文是 JavaScript 高级深入浅出系列的第八篇,将认识到 JS 中函数式编程相关知识

正文

函数式编程是 JS 中的一种编程范式

1. JS 中的纯函数

函数式编程中有一个非常重要的概念:纯函数,JS 符合函数式编程范式,所以也有纯函数的概念

  • 在 react 开发中纯函数是多次被提及的
  • 比如 react 中组件就被要求像是一个纯函数(为什么像,因为还有 class 组件),redux 中有一个 reducer 的概念,也是要求必须是一个纯函数
  • 所以掌握纯函数对于理解很多框架的设计是非常重要的

纯函数维基百科定义:

  • 在程序设计中,若一个函数符合以下条件,那么这个函数就被称为纯函数:
  • 此函数在相同的输入值时,需产生相同的输出
  • 函数和输出和输入值以外的其他隐藏信息或状态无关,也和由 I/O 设备产生的外部输出无关
  • 该函数不能有语义上可观察的函数副作用,诸如**“触发事件”**,使输出设备输出,或更改输出值以外物件的内容

简单的理解来说,一个纯函数内部的任何处理都是对于输入参数的处理,不可对函数之外做任何操作

1.1 副作用的理解

  • 副作用(side effect)本身是一个医学的概念,比如吃了什么药,本来是为了治病,但是造成了其他的副作用
  • 在计算机科学的领域中,副作用泛指在执行函数时,除了返回函数值之外,还对调用函数产生了附加影响,例如修改了全局变量,修改参数或者修改外部的存储

纯函数在执行过程中是不允许出现副作用的:

  • 副作用往往是产生 bug 的“温床”

我们为了能够编写健壮的函数,最好让一个函数负责一个功能

1.2 纯函数的例子

  • Array.prototype.slice:对数组进行截取,并返回一个截取的新数组(是一个纯函数)
  • Array.prototype.splice:对数组进行截取,返回截取的新数组,并更改原数组(不是一个纯函数)
// 这个不是一个纯函数,这个是有一点容易混淆的,因为 bar 函数将参数更改了,间接把外部的值也修改了
function bar(info) {
    info.age = 20
}
let obj = { age: 18 }
bar(obj)
// 这个函数严格意义上来说也不算是一个纯函数,因为纯函数是要除了 return 之外不能对外界有任何输出
// 但是可以理解为一个纯函数,因为这个函数没有副作用
function printInfo(info) {
    console.log(info.name)
}

1.3 纯函数的优势

为什么纯函数式编程中这么重要:

  • 可以安心的编写和使用
  • 在写的时候保证了纯度,专注于逻辑即可,不用担心是否对外界造成影响
  • 在用的时候,确定此函数不会造成副作用,并且确定的输入一定会有确定的输出
  • 所有 React 组件都必须像纯函数一样保护它们的 props 不被修改

2. JS 柯里化

柯里化是函数式编程的重要概念。

柯里化维基百科定义:

  • 在计算机科学中,柯里化(Currying)是把接收多个参数的函数,变成接收一个单个参数(最初函数的第一个参数)的函数,并且返回接收余下的参数,而且返回结果的新函数的技术
  • 柯里化声称“如果你固定某些参数,你将得到接受余下参数的一个函数”

简单来说就是:

  • 只传递给函数一部分参数,让它返回一个函数处理剩余的参数。这个过程称为柯里化
// 普通的函数
function sum(x, y, z) {
  return x + y + z
}
sum(10, 20, 30)
// 柯里化函数

function sum2(x) {
  return function(y) {
    return function(z) {
      return x + y + z
    }
  }
}
sum2(10)(20)(30)

简化柯里化代码

const sum2 = x => y => z => x + y + z

2.1 柯里化的作用一:让函数的职责单一

为什么需要柯里化?

  • 在函数式编程中,我们往往希望一个函数处理的问题尽可能的单一,而不是将一大堆的处理过程交给一个函数来处理。
  • 那么我们是否就可以将每次传入的参数在单一的函数中处理,处理完后在下一个函数中再使用处理后的结果

比如上面的例子参数,我们想让 1 函数 + 2,2 函数 * 2,3 函数 ** 2

function sum(x, y, z) {
  x = x + 2
  return function(y) {
    y = y * 2
    return function(z) {
      z = z * z
      return x + y + z
    }
  }
}

console.log(sum(10)(20)(30))

2.2 柯里化作用二:逻辑复用

// 比如我们有一个 log 函数
function log(date, type, msg) {
  console.log(`[${date.getHours()}:${date.getMinutes()}][${type}][${msg}]`)
}
log(new Date(), 'DEBUG', '查找到代码 bug')
log(new Date(), 'DEBUG', '查找到代码 bug2')
log(new Date(), 'DEBUG', '查找到代码 bug3')

// 柯里化的优化
const curryingLOG = date => type => msg =>
  console.log(`[${date.getHours()}:${date.getMinutes()}][${type}][${msg}]`)

// 这样可以做到逻辑复用
const DEBUG_LOG = msg => curryingLOG(new Date())('DEBUG')(msg)

DEBUG_LOG('查找到代码 bug')

2.3 自动柯里化函数的实现

前置知识:

function foo(x, y, z, m, n) {}
console.log(foo.length) // 该函数接受参数的个数,结果为 5 

实现:

// 需求:传入一个函数,通过调用自定方法获取到柯里化的函数
function add(num1, num2, num3) {
  return console.log(num1 + num2 + num3)
}

function fnCurrying(fn) {
  return function curried(...args) {
    // 1. 当判断传入的参数 >= 需要传的参数 时,直接执行函数
    if (args.length >= fn.length) {
      return fn.apply(this, args)
    } else {
      // 2. 没有达到参数时,需要递归检查是否达到了参数的个数
      return function(...args2) {
        return curried.apply(this, [...args, ...args2])
      }
    }
  }
}

const curriedAdd = fnCurrying(add)
// curriedAdd(10, 20, 30)  // 60
// curriedAdd(10, 20)(30)  // 60
// curriedAdd(10)(20)(30)  // 60

3. 组合函数

组合(compose)函数是在 JS 开发过程中对于函数的一种使用技巧。

  • 比如我们现在需要对某个数据进行函数调用,执行函数fn1fn2,这两个函数依次执行
  • 但是如果我们每次都要调用这两个函数,就显得非常重复
  • 我们就可以将这两个函数组合起来,调一次就可以依次执行两个函数
  • 对于函数的组合,加作组合函数(Compose Function)
function double(n) {
  return n * 2
}

function square(n) {
  return n ** 2
}

let num = 10

// 需要每次调用两个函数,就很繁琐
num = square(double(num))

// 写一个工具函数
function composeFn(m, n) {
  return function(num) {
    return m(n(num))
  }
}

const fn = composeFn(double, square)

fn(10)
// 一个复杂的案例,不知道具体会传入几个参数
function _componse(...fns) {
  fns.map(fn => {
    if (typeof fn !== 'function') {
      throw new TypeError('Expected arguments are functions')
    }
  })
  return function(...args) {
    let result = null
    // 这里核心逻辑就是,index = 0 也就是 第一个方法,通过传入的参数来调用函数
    // 之后的函数都是通过上一个函数的返回值作为参数去调用
    // 使用 call 而不是直接调用,防止隐式或显式修改了 this 指向
    fns.map((fn, index) => {
      result = index ? fn.call(this, result) : fn.call(this, ...args)
    })
    return result
  }
}

function add(num) {
  return num + 1
}

function double(num) {
  return num * 2
}

const fn = _componse(add, double)
console.log(fn(2))

总结

本文中,你学到了 3 个知识点:

  • 了解到函数式编程范式,并学习了纯函数相关知识
  • 了解了柯里化与柯里化的作用,并手写自动柯里化函数
  • 了解了组合函数,并手写了组合函数工具函数