探索函数式编程:纯函数 | 高阶函数 | 函数柯里化 | 组合函数

函数式编程

概述

定义

  • 函数式编程(FP : Functional programming)是一种范式,强调使用函数来构建程序,并且避免使用状态改变和可变数据(避免函数的执行存在副作用) → 范式,用函数来 "组合" 以及 "处理数据"(将运算过程抽象成函数)复用

特点

  • 函数是第一等公民:在函数式编程语言中,函数可以被当作变量一样赋值给变量,作为参数传递给其他函数,或者作为其他函数的返回值
  • 纯函数:纯函数是函数式编程的核心。纯函数对于相同的输入总是返回相同的输出,并且不依赖于或修改外部状态(即没有副作用)
  • 不可变性:尽量使用不可变的数据结构。一旦创建,数据就不能被修改。这有助于避免并发问题,因为数据是共享的,但不会被修改
  • 高阶函数:高阶函数是接受函数作为参数或返回函数的函数。这允许函数被用作数据,并且可以在程序中以更灵活的方式组合和重用
  • 递归:由于避免使用可变状态和循环结构,函数式编程倾向于使用递归作为主要的控制结构
  • 惰性求值:一些函数式编程语言支持惰性求值(也称为非严格求值),即表达式仅在需要其结果时才进行计算。这有助于优化性能和内存使用

优势

  • 易于理解和维护:由于避免了状态变化和副作用,函数式代码通常更加清晰和简洁
  • 模块化:函数式编程鼓励将大问题分解为小问题,并通过组合纯函数来解决它们
  • 易于测试:纯函数不依赖于外部状态,因此可以独立于其他部分进行测试
  • 并发和并行:由于避免了共享状态,函数式编程更易于实现并发和并行计算
  • 总: 避免了副作用,方便维护, 方便测试,并发执行,可以组合(方便扩展)

通过几种编程方式来初识一下函数式编程

  • const arr = [1, 2, 3, 4, 5]
    // -- 示例: 计算 arr 数组的总和 → 如下
    
  • // pp: 面向过程 → 一步一步根据需求编写相应代码
        let total = 0
        for (let i = 0; i < arr.length; i++) {
            total += arr[i]
        }
        console.log(total)
    
  • // oop: 面向对象 → 将逻辑分解成对象,对象可以完成自己的功能与自身的一些行为(实例,继承,封装,多态,扩展)
        class Calc {
            constructor() {
                this.total = 0
            }
    
            sum(arr) { // -- 求和方法
                for (let i = 0; i < arr.length; i++) { // -- 运算过程
                    this.total += arr[i]
                }
                return this.total
            }
        }
        const calc = new Calc()
        console.log(calc.sum(arr))
    
        // -- 但因为 JS 只能是单个继承,且继承会使功能逻辑变得复杂
    
  • // fp: 函数式编程 → 相当于数学意义上的 y = f(x) 不关心内部处理逻辑,可以让多个函数组合起来一起使用(组合高于继承:更加灵活)
        const total = arr.reduce((prev, current) => prev + current, 0) // -- 如: reduce 函数中有实现了具体的累加逻辑,但我们可以不需要关心是如何实现的,只需将自己的逻辑和 reduce 组合起来使用即可(高阶函数) → 高阶函数 + 纯函数 : 实现函数式编程
        console.log(total)
    
  • 核心: 函数式编程的核心就是组合,把函数变成一个个小的函数再进行组合再一起

核心概念

纯函数(Pure Function): 再函数式编程中纯函数是一个非常重要的概念

  • 相关特性

    • 给定相同的输入,总是返回相同的输出纯函数不依赖于或修改程序状态之外的任何数据,也不受外部输入(如用户输入、文件I/O、数据库查询等)的影响 → 因此,对于相同的输入,纯函数将始终产生相同的输出(反之就不属于一个纯函数)
    • 不产生副作用副作用是指函数执行过程中除了返回值之外对程序状态产生的任何修改。纯函数不修改外部状态(如全局变量、数据库记录、文件内容等),也不进行I/O操作(如打印到控制台、读取文件等)
    • 返回值仅依赖于输入纯函数的输出仅由其输入参数决定,与函数被调用的时间、次数或程序中的其他状态无关
    • 可预测性由于纯函数的行为完全由其输入决定,并且不产生副作用,因此它们的输出是完全可预测的
  • 好处

    • 易于测试由于纯函数不依赖于外部状态,因此可以独立于程序的其他部分进行测试
    • 可重用性纯函数可以在不同的上下文中重复使用(缓存),因为它们的行为是确定的
    • 并行化纯函数可以安全地并行执行,因为它们不共享或修改外部状态
    • 总: 可以缓存, 方便测试, 纯函数可以并发执行 → 状态管理都会采用纯函数的方式(如 redux)
  • 示例

    • function sum(a,b) { // -- 属于函数式编程: 给定输入对应的输出,且没有对代码产生副作用...
          return a + b
      }
      
    • function getTime() { // -- 不属于纯函数: 没有输入且输出结果不确定
          const date = new Date()
          return date.toLocaleDateString()
      }
      
    • let i = 0
      function fn() { // -- 不属于纯函数: 多次调用结果不同且没有输入(影响了外部变量,存在副作用 : 副作用会降低方法使用的通用性)
          return ++i
      }
      

高阶函数(Higher-Order Function):

  • 定义 : 只要满足如下两个条件的任意一条或都满足时,就称之为高阶函数

    • 接受一个或多个函数作为输入高阶函数可以接受其他函数作为参数

    • 返回一个函数高阶函数可以返回一个函数作为输出

    • + 简单示例
      + 接受函数作为参数: map、filter 和 reduce 方法,它们都接受一个函数作为参数 -- 其它自定义同理
          const doubled = numbers.map(function(number) {  
              return number * 2;  
          }); 
      
      + 返回一个函数的例子  
          function createMultiplier(multiplier) {  
              return function(x) {  
                  return multiplier * x;  
              };  
          }
      
  • 练习

    • + 接收一个或多个函数作为参数: 如我们前面在概述中提到的 reduce 函数就是一个高阶函数(接收一个函数)
      + 简单实现:
      	Array.prototype.reduce = function (cb, startValue) { // -- 属于高阶函数: 接收一个 cb 函数作为参数
              const hasStartValue = typeof startValue !== "undefined"
      
              let acc = hasStartValue ? startValue : this[0]
              let index = hasStartValue ? 0 : 1
      
              for (let i = index; i < this.length; i++) {
                  acc = cb(acc, this[i], i, this) // -- 抽象运算过程
              }
      
              return acc
          }
      
      
    • + 返回一个函数: 通过高阶函数,实现一个简单的缓存功能
          function memoize(coreFn, resolver) { // -- coreFn:核心函数  resolver?:自定义缓存规则函数(返回值返回对应的 key 值)
              const cache = new Map() // -- 缓存表
              
              return function (...args) {
                  // -- 每次调用函数,都重新创建一个对应的缓存 key,后续永 key 进行判断是否已经缓存过了
                  const cacheKey = typeof resolver === "function" ? resolver(...args) : args[0] // -- 判断用户是否传入自定义缓存 key,如果没有则按默认只取第一个参数作为缓存 key 
      
                  let cacheResolve = cache.get(cacheKey) // -- 根据 cacheKey 获取对应的缓存数据 → 如果没有 ↓
                  if (!cacheResolve) { // -- 当没有相应的缓存信息时,执行 coreFn 原方法,并将其缓存起来
                      cacheResolve = coreFn(...args)
                      cache.set(cacheKey, cacheResolve)
                  }
      
                  return cacheResolve
              }
          }
      
      + TEST
          function sum(a, b) {
              console.log("真实执行")
              return a + b
          }
      
          const memoizedSum = memoize(sum, (...args) => {
              return JSON.stringify(args) // -- 定义缓存规则: 按照所有的参数进行缓存 → 默认只会按照第一个参数进行缓存(不常用)
          })
      
          console.log(memoizedSum(1, 3))
          console.log(memoizedSum(2, 3))
          console.log(memoizedSum(1, 6))
          console.log(memoizedSum(1, 3))
      

函数柯里化(Currying)

  • 定义

    • 柯里化: 将多参数的函数转化成单参数函数(标准的柯里化强调的是,转化后的函数参数是一个一个的 : 如果是分批传入的称只为偏函数)

    • 偏函数: 先固定一部分参数,返回一个函数且包含剩余的参数

    • + 简单示例
      function sum1(a, b, c) { // -- 原函数
          return a + b + c
      }
      
    • function sum2(a) { // -- 柯里化函数
          return function (b) {
              return function (c) {
                  return a + b + c
              }
          }
      }
      
    • function sum3(a) { // -- 偏函数(但多数也将该称之为柯里化函数)
          return function (b, c) {
              return a + b + c
          }
      }
      
  • 练习: 定义一个类型判断函数

    • + 原函数方式
          function isType(typing, value) { // -- 原函数
              return Object.prototype.toString.call(value) === `[object ${typing}]`
          }
      
          // -- 原函数使用示例(代码过于重复 -- 通过柯里化函数进行解耦操作)
          console.log(isType("String", "deng"))
          console.log(isType("String", "kong"))
          console.log(isType("String", 111))
      
    • + 函数柯里化
          function isType(typing) { // -- 柯里化函数
              return function (value) {
                  return Object.prototype.toString.call(value) === `[object ${typing}]`
              }
          }
      
          const isString = isType("String")
      	
          // -- 使用柯里化函数,我们可以将注意力更集中再对应的 value 上,且代码不会相同的逻辑过于重复...
          console.log(isString("deng"))
          console.log(isString("kong"))
          console.log(isString(123))
      
  • 实现一个通用的柯里化函数: 判断传入的参数个数,如果个数小于函数参数的个数,那么返回一个新函数继续等待剩余函数,如果大于等于时,则执行相应函数

    • function curry(fn) {
          return function curried(...args) {
              if (args.length < fn.length) { // -- 1. 判断传入的参数个数,如果个数小于函数参数的个数,那么返回一个新函数继续等待剩余函数
                  return (...other) => curried(...args, ...other)
              }
              return fn(...args) // -- 2. 如果大于或等于时,则执行相应函数
          }
      }
      
    • + TEST
          function sum(a, b, c, d) {
              return a + b + c + d
          }
          const curriedSum = curry(sum)
          console.log(
              curriedSum(1, 1)(2)(3)
          )
      

函数组合(Compose)

定义

  • 将多个简单的函数组合成一个新函数 ,从而以链式的方式来处理数据(tip: 指一个函数的输出作为另一个函数的输入,从而创建出一个新函数)

特点

  • 参数是函数函数组合的参数必须是函数,返回的结果也是一个函数
  • 自右向左执行在函数组合中,函数通常是从右向左执行的,即最右边的函数最先执行,其输出作为下一个函数的输入,依此类推

练习

  • 示例函数

    • function doubleValue(n) { // -- 1. x2
          return n + n
      }
      
      function toFixed(n) { // -- 2. 保留两位小数
          return n.toFixed(2)
      }
      
      function addPrefix(n) { // -- 3. 添加 "$" 前缀
          return "$" + n
      }
      
  • 不使用组合函数

    • console.log(
          addPrefix(
              toFixed(
                  doubleValue(100)
              )
          )
      ) // log: $200.00
      
  • 使用 rodash 中的 flowRight 组合函数

    • import _ from "loadsh"
      const compose = _.flowRight(addPrefix, toFixed, doubleValue) // -- 通过 loadsh 中的 flowRight 将前面的小函数进行组合,生成对应的组合函数 compose
      console.log(compose(100)); // log: $200.00
      
      

组合函数的实现

  • 组合函数通常可以使用数组中的 reduceRight 或使用 递归 等方式进行实现,我们这里演示的是通过 reduceRight 方法的实现方式 → 当然也可以直接使用 loadsh 等库中的的组合函数

  • + 实现组合函数
    function flowRight(...fns) { // -- 1. 自定义一个组合函数,接收多个小函数
        if (fns.length === 0) return
    
        return fns.reduceRight((prev, cur) => { // -- 2. 通过数组中的 reduceRight 方法,从右向左遍历函数数组,并依次将函数的输出作为下一个函数的输入,从而实现函数组合
            return (...args) => cur(prev(...args))
        })
    }
    
  • + TEST
    const compose = flowRight(addPrefix, toFixed, doubleValue) // -- 使用我们自定义的 flowRight 组合函数,对上面 3 个小函数进行组合
    console.log(compose(100))
    

练习2(函数柯里化与组合函数): 将一个类似 "h kong" 这样的字符转换成 "H_KONG" 该格式的字符

  • + 测试字符
    	const text = "h kong"
    
  • + pp: 面向过程的方式
        const flow1 = text.split(" ")
        const flow2 = flow1.join("_")
        const flow3 = flow2.toUpperCase()
        console.log(flow3)
    
  • + nf: 定义一个普通的函数来实现相应的功能
        function handleString(str, splitStr, joinStr) {
            return str.split(splitStr).join(joinStr).toUpperCase()
        }
        console.log(handleString(text, " ", "_"))
    
  • + Curring: 使用函数柯里化
    	const curriedStrFn = curry((str, splitStr, joinStr) => {
            return str.split(splitStr).join(joinStr).toUpperCase()
        })
        console.log(curriedStrFn(text)(" ")("_"))
    
  • + Curring & Compose:函数柯里化结合组合函数进行实现 
        const split = curry((sep, str) => str.split(sep)) // -- 使用 curry 柯里化函数,将每个步骤的函数进行柯里化
        const join = curry((sep, str) => str.join(sep))
        const upper = str => str.toUpperCase()
    
        const compose = flowRight(upper, join("_"), split(" ")) // -- 使用 flowRight 组合函数,将每个小函数进行组合
        
        console.log(compose("h kong")) // -- TEST
    

你可能感兴趣的:(前端,javascript)