JS Advance --- 函数式编程

206 阅读6分钟

如果一个编程语言的编程范式(编程方法,编程规范)是函数是一等公民,那么就可以认为这个编程语言使用的是函数式编程

在函数式编程中,函数可以作为参数或者返回值使用,在另一个函数中进行传递

纯函数

函数式编程中有一个非常重要的概念叫纯函数(pure function),JavaScript符合函数式编程的范式,所以也有纯函数的概念

纯函数必须满足如下两个条件:

  1. 相同的输入,一定会产生确定的输出
  2. 函数在执行过程中,不能产生副作用
    • 函数的结果只取决于传入的函数参数和内部逻辑
    • 函数的执行和I/O设备的输入输出无关
    • 狭义上,函数中如果存在console.logconsole.log是副作用,但是广义上,console.log不是副作用

副作用(side effect)表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量,修改了引用类型的参数或者改变了外部的存储

如果一个函数产生了副作用,那么就意味着修改了函数之外的值,此时就可以导致其它成员在使用对应值的时候,容易产生bug

当然JavaScript中的函数并不是全部都要写成纯函数,很多时候,我们的确需要书写非纯函数,但是能使用纯函数的时候,还是推荐使用纯函数

示例

JS对数组的截取有2个截取数组的方法slicesplice

  • slice截取数组时不会对原数组进行任何操作,而是生成一个新的数组 ---> 纯函数

  • splice截取数组, 会返回一个新的数组, 也会对原数组进行修改 ---> 非纯函数

slice

const arr = [1, 2, 3, 4, 5]
// slice的结果只取决于start和end这两个传入的参数
// 所以slice是一个纯函数
const res = arr.slice(2)

console.log(res) // => [3, 4, 5]
console.log(arr) // => [1, 2, 3, 4, 5]

splice

const arr = [1, 2, 3, 4, 5]
// splice会修改原数组,而这就是splice函数产生的副作用
// 因此splice函数不是一个纯函数
const res = arr.splice(2)

console.log(res) // => [3, 4, 5]
console.log(arr) // => [1, 2]

示例

// foo是一个纯函数
function foo(info) {
  return {
    ...info,
    age: 23
  }
}

console.log(foo({
  name: 'Klaus',
  age: 18
}))
const num = 12

// foo函数依赖了全局的变量num
// 也就是说当num的值发生了改变的时候,函数的相同参数的返回值会不一样
// 所以一个函数如果依赖了外部的变量来进行逻辑运算,那么这个函数就不是一个纯函数
// 因此foo函数并不是一个纯函数
function foo(v) {
  return v + num
}

console.log(foo(3))
// Cpn是一个纯函数
// 在react等框架中就要求我们尽可能的像纯函数一样
// 来保证我们的props不会被修改,这其实和vue中的单向数据流的概念是很像的
function Cpn(props) {
  return {...props}
}

函数柯里化

  • 将一个有多个参数的函数转变为接收一部分参数的函数
  • 接收一部分参数的函数又返回一个函数去处理剩余的参数
  • 这个转换的过程就被称之为函数柯里化(Currying)
function sum(num1, num2, num3, num4) {
  return num1 + num2 + num3 + num4
}

console.log(sum(10, 20, 30, 40))
// 转换方式1
const sum = num1 => num2 => num3 => num4 => num1 + num2 +  num3 + num4
console.log(sum(10)(20)(30)(40))
// 转换方式2
const sum = (num1, num2) => (num3, num4) => num1 + num2 +  num3 + num4
console.log(sum(10, 20)(30, 40))

只要多个参数的函数转换为多次函数调用的过程 就被称之为函数柯里化

柯里化的作用

  • 单一职责原则: 在函数式编程中,我们其实往往希望一个函数处理的问题尽可能的单一,而不是将一大堆的处理过程交给一个 函数来处理
  • 函数公共逻辑复用
// 不使用柯里化
const printLog = (date, type, message) => `[${date.getHours()}]:[${date.getMinutes()}]:[${type}]:[${message}]`

// 在下边的代码中,短时间内打印的日期都是一样的,而且日志类型都是一样的
// 所以其实这部分代码是可以复用的
console.log(printLog(new Date(), 'DEBUG', 'something error'))
console.log(printLog(new Date(), 'DEBUG', 'something error'))
console.log(printLog(new Date(), 'DEBUG', 'something error'))
console.log(printLog(new Date(), 'DEBUG', 'something error'))
const printLog = date => type => message => `[${date.getHours()}]:[${date.getMinutes()}]:[${type}]:[${message}]`

printNowLog = printLog(new Date())

printErrorLog = printNowLog('ERROR')
printWarningLog = printNowLog('WARNING')


console.log(printErrorLog('something error'))
console.log(printErrorLog('something error'))
console.log(printErrorLog('something error'))

console.log(printWarningLog('something warning'))
console.log(printWarningLog('something warning'))
console.log(printWarningLog('something warning'))
  • 函数功能定制
// 以下是一些伪代码
const vue = () => {
  function render() {
    // 这里实际将vnode渲染为dom的渲染器
  }

  return {
    render,
    createApp: createAppAPI(render)
  }
}

function createAppAPI(render) {
  // 这里是将App组件对象转换为VNode的代码

  return function(App) {
    // 这里就类似于createAppAPI(render)(App)
    // 这样一方面保证了各个函数内部的职责单一
    // 另一方面确保了vue的render是独立的
    // 我们可以使用的是渲染成DOM的render函数
    // 也可以调用的是渲染成APP控件的render函数
    render(App)
  }
}

// 实际调用
import { createApp } from 'vue'

createApp(App).mount('#app')

模拟柯里化转换函数

function foo(num1, num2) { ... }
console.log(foo.length) // => 2
console.log(foo.name) // => 'foo'                        
const currying = fn => {
  // 这里需要返回一个具名函数,因为后续需要递归调用
  return function curried(...args) {
    // 如果收集到了足够多的参数的时候,直接调用函数
    if (args.length >= fn.length) {
      // 使用apply或call调用是为了避免外部调用的时候修改了实际调用的this指向
      return fn.apply(this, args)
    } else {
      // 如果没有接收到足够多的参数的时候,返回一个新的函数,继续接收参数
      return function(...params) {
        // 参数合并后递归调用
        // 使用apply的目的是为了避免调用函数的时候,调用者显示绑定了this的值
        // 所以在高阶函数封装的时候,内部函数调用的this一般需要显示指定
        return curried.apply(this, [...args, ...params])
      }
    }
  }
}

const sum = (num1, num2, num3) => num1 + num2 + num3

const currySum = currying(sum)

console.log(currySum(10, 20, 30)) // => 60
console.log(currySum(10, 20)(30)) // => 60
console.log(currySum(10)(20)(30)) // => 60

const tmp = currySum(10)
console.log(tmp.call({}, 20, 30))

组合函数

组合(Compose)函数是在JavaScript开发过程中一种对函数的使用技巧

比如我们现在需要对某一个数据进行函数的调用,执行两个函数fn1和fn2,这两个函数是依次执行的

那么如果每次我们都需要进行两个函数的调用,操作上就会显得重复

那么是否可以将这两个函数组合起来,自动依次调用呢?

这个过程就是对函数的组合,我们称之为 组合函数(Compose Function)

function compose(...fns) {
  if (!fns.length) {
    throw new TypeError('Function compose need arguments')
  }

  for (let fn of fns) {
    if (typeof fn !== 'function') {
      throw new TypeError('Exception arguments are functions')
    }
  }

  // 组合函数返回一个新的函数
  // 在这个新的函数中依次调用我们需要调用的函数
  return function(...args) {
    let index = 0
    // 第一个函数可以接收任意个数的参数
    let res = fns[index].call(this, ...args)

    while(++index < fns.length) {
      // 从第二个参数起,接收的参数为上一个参数的返回值
      res = fns[index].call(this, res)
    }

    return res
  }
}

const double = num => num * 2
const square = num => num ** 2
const fn = compose(double, square)

console.log(fn(3)) // => 36