• 深入剖析JavaScript(一)——函数式编程


    为什么要学习函数式编程

    Vue进入3.*(One Piece 海贼王)世代后,引入的setup语法,颇有向老大哥React看齐的意思,说不定前端以后还真是一个框架的天下。话归正传,框架的趋势确实是对开发者的js功底要求更为严格了,无论是hooks、setup,都离不开函数式编程,抽离代码可复用逻辑,更好地组织及复用代码,有一点我感到很高兴的是,终于可以抛弃烦人的this了,当然,这也不是我为偷懒而生出这样的感想,人家道格拉斯老爷子可是在他的新书《JavaScript悟道》里极力吐槽了一下this,所以,也算是像js大佬看齐了。所以,要想不被前端日新月异的新技术给冲昏头脑,还是适时回来重学一下JavaScript吧。


    什么是函数式编程

    函数式编程(Functional Programming, FP),FP 是编程范式之一,我们常听说的编程范式还有面向过程编程、面向对象编程。

    • 面向对象编程:面向对象有三大特性,通过封装、继承和多态来演示事物之间的联系,如果更宽泛来说,抽象也应该算进去,但是由于面向对象的本质就是抽象,其不算是三大特性也不为过。
    • 函数式编程:函数式编程的思想主要就是对运算过程进行抽象,它更像一个黑盒,你给入特定的输出,进过黑盒运算后再返回运算结果。你可以将其理解为数学中的y = f(x)。
      • 程序的本质:根据输入进行某种运算得到相应的输出。
      • x -> f(联系、映射) -> y, y = f(x)
      • 函数式编程中的函数其实对应数学中的函数,即映射关系。
      • 相同的输入始终要得到相同的输出(纯函数)
      • 可复用

    前置知识

    函数是一等公民

    作为一名有一定经验的前端开发者,你一定对JavaScript中“函数是一等公民”这一说法不陌生。

    这里给出权威文档MDN的定义:当一门编程语言的函数可以被当作变量一样用时,则称这门语言拥有头等函数。例如,在这门语言中,函数可以被当作参数传递给其他函数,可以作为另一个函数的返回值,还可以被赋值给一个变量。

    函数可以储存在变量中

    let fn = function() {
      console.log('Hello First-class Function')
    }
    fn()
    
    • 1
    • 2
    • 3
    • 4

    函数作为参数

    function foo(arr, fun) {
      for (let i = 0; i < arr.length; i++) {
        fun(arr[i])
      }
    }
    
    const array = [1, 2, 3, 4]
    foo(array, function(a) { console.log(a) })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    函数作为返回值

    function fun() {
      return function () {
        consoel.log('哈哈哈')
      }
    }
    const fn = fun()
    fn()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    高阶函数

    什么是高阶函数

    • 高阶函数

      • 可以把函数作为参数传递给另外一个函数
      • 可以把函数作为另外一个函数的返回结果
    • 函数作为参数(为了避免文章篇幅过长,后面的演示代码就不给出测试代码了,读者可自行复制文章代码在本地编辑器上调试)

      function filter(array, fn) {
          let results = []
          for (let i = 0; i < array.length; i++) {
              if (fn(array[i])) {
                  results.push(array[i])
              }
          }
          return results
      }
      
      // 测试
      let arr = [1, 3, 4, 7, 8]
      const results = filter(arr, function(num) {
          return num > 7
      })
      console.log(results) // [8]
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
    • 函数作为返回值

      // 考虑一个场景,在网络延迟情况下,用户点击支付,你一定不想要用户点完支付没反应后点击下一次支付再重新支付一次,不然,你的公司就离倒闭不远了。
      // 所以考虑一下once函数
      function once(fn) {
          let done = false
          return function() {
              if (!done) {
                  done = true
                  return fn.apply(this, arguments)
              }
          }
      }
      
      let pay = once(function (money) {
          console.log(`支付: ${money} RMB`)
      })
      pay(5)
      pay(5)
      pay(5)
      pay(5)
      // 5
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20

    使用高阶函数的意义

    • 抽象可以帮我们屏蔽细节,只需要关注目标
    • 高阶函数是用来抽象通用的问题

    常用高阶函数

    • forEach(已实现)
    • map
      const map = (array, fn) => {
        let results = []
        for (let value of array) {
          results.push(fn(value))
        }
        return results
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
    • filter
    • every
      const every = (array, fn) => {
        let result = true
        for (let value of array) {
          result = fn(value)
          if (!result) {
            break
          }
        }
        return result
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
    • some
      const some = (array, fn) => {
        let result = false
        for (let value of array) {
          result = fn(value)
          if (result) {
            break
          }
        }
        return result
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
    • find/findIndex
    • reduce
    • sort

    闭包

    闭包 (Closure):函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包。

    闭包的本质:函数在执行的时候会放到一个执行栈上当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员。

    function makePower(power) {
        return function (num) {
            return Math.pow(num, power)
        }
    }
    
    // 求平方及立方
    let power2 = makePower(2)
    let power3 = makePower(3)
    
    console.log(power2(4)) // 16
    console.log(power2(5)) // 25
    console.log(power3(4)) // 64
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    function maekSalary(base) {
        return function (performance) {
            return base + performance
        }
    }
    
    let salaryLevel1 = makeSalary(12000)
    let salaryLevel2 = makeSalary(15000)
    
    
    console.log(salaryLevel1(2000)) // 14000
    console.log(salaryLevel2(3000)) // 18000
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    其实上面这两个函数都是差不多的,都是通过维持对原函数内部成员的引用。具体可以通过浏览器调试工具自行了解。

    纯函数

    纯函数概念

    • 纯函数:相同的输入永远会得到相同的输出

    lodash 是一个纯函数的功能库,提供了对数组、数字、对象、字符串、函数等操作的一些方法。有人可能会有这样的疑惑,随着ECMAScript的演进,lodash中很多方法都已经在ES6+中逐步实现了,那么学习其还有必要吗?其实不然,lodash中还是有很多很好用的工具函数的,比如说,防抖节流是前端工作中经常用到的,你可不想每次都手写一个函数吧?更何况没有一点js功底还写不出来呢。

    话归正传,来看看数组的两个方法:slice和splice。

    • slice 返回数组中的指定部分,不会改变原数组
    • splice 对数组进行操作返回该数组,会改变原数组
    let array = [1, 2, 3, 4, 5]
    
    // 纯函数
    console.log(array.slice(0, 3))
    console.log(array.slice(0, 3))
    console.log(array.slice(0, 3))
    
    // 不纯的函数
    console.log(array.splice(0, 3))
    console.log(array.splice(0, 3))
    console.log(array.splice(0, 3))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    纯函数的好处

    • 可缓存

      • 因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来
        function getArea(r) {
            console.log(r)
            return Math.PI * r * r
        }
        
        function memoize(f) {
            let cache = {}
            return function() {
                let key = JSON.stringify(arguments)
                cache[key] = cache[key] || f.apply(f, arguments)
                return cache[key]
            }
        }
        
        let getAreaWithMemory = memoize(getArea)
        console.log(getAreaWithMemory(4))
        console.log(getAreaWithMemory(4))
        console.log(getAreaWithMemory(4))
        // 4
        // 50.26548245743669
        // 50.26548245743669
        // 50.26548245743669
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
        • 19
        • 20
        • 21
        • 22
    • 可测试

      • 纯函数让测试更方便
    • 并行处理

      • 在多线程环境下并行操作共享的内存数据很可能会出现意外情况
      • 纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数 (Web Worker)

    副作用

    // 不纯的
    let mini = 18
    function checkAge (age) {
      return age >= mini
    }
    // 纯的(有硬编码,后续可以通过柯里化解决)
    function checkAge (age) {
      let mini = 18
      return age >= mini
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    副作用让一个函数变的不纯(如上例),纯函数的根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用。

    柯里化

    柯里化的概念:当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变),然后返回一个新的函数接收剩余的参数,返回结果。

    柯里化就可以解决上面代码中的硬编码问题

    // 普通的纯函数
    function checkAge(min, age) {
        return age >= min
    }
    
    // 函数的柯里化
    function checkAge(min) {
        return function(age) {
            return age >= min
        }
    }
    // 当然,上面的代码也可以用ES6中的箭头函数来改造
    const checkAge = (min) => (age => age >= min)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    下面来手写一个curry函数

    function curry(func) {
      return function curriedFn(...args) {
        if (args.length < func.length) {
          return function() {
            return curriedFn(...args.concat(Array.from(arguments)))
          }
        }
        return func(...args)
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    函数组合

    看了这么多代码,你肯定会觉得函数里面有很多return看起来不是很好看,事实也确是如此,所以这就要引出函数组合这个概念。

    • 纯函数和柯里化很容易写出洋葱代码 h(g(f(x)))
      • 获取数组的最后一个元素再转换成大写字母, .toUpper(.first(_.reverse(array))) (这些都是lodash中的方法)
    • 函数组合可以让我们把细粒度的函数重新组合生成一个新的函数

    你可以把其想象成一根管道,你将fn管道拆分成fn1、fn2、fn3三个管道,即将不同处理逻辑封装在不同的函数中,然后通过一个compose函数进行整合,将其变为一个函数。

    fn = compose(f1, f2, f3)
    b = fn(a)
    
    • 1
    • 2

    Functor(函子)

    什么是Functor

    • 容器:包含值和值的变形关系(这个变形关系就是函数)
    • 函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有 map 方法,map 行一个函数对值进行处理(变形关系)
    // Functor 函子 一个容器,包裹一个值
    class Container {
        constructor(value) {
            this._value = value
        }
    
        // map 方法,传入变形关系,将容器里的每一个值映射到另一个容器
        map(fn) {
            return new Container(fn(this._value))
        }
    }
    
    let r = new Container(5)
        .map(x => x + 1)
        .map(x => x * x)
    
    console.log(r)  // 36
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 总结
      • 函数式编程的运算不直接操作值,而是由函子完成
      • 函子就是一个实现了 map 契约的对象
      • 我们可以把函子想象成一个盒子,这个盒子里封装了一个值
      • 想要处理盒子中的值,我们需要给盒子的 map 方法传递一个处理值的函数(纯函数),由这
        个函数来对值进行处理
      • 最终 map 方法返回一个包含新值的盒子(函子)

    可能你不习惯在代码中看到new关键字,所以可以在容器中实现一个of方法。

    class Container {
        static of (value) {
            return new Container(value)
        }
    
        constructor(value) {
            this._value = value
        }
    
        map(fn) {
            return Container.of(fn(this._value))
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    MayBe 函子

    上面的代码中如果传入一个null 或 undefined的话,代码就会抛出错误,所以需要再实现一个方法

    class MayBe {
        static of(value) {
            return new MayBe(value)
        }
    
        constructor(value) {
            this._value = value
        }
    
        map(fn) {
            return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
        }
    
        isNothing() {
            return this._value == null // 此处双等号等价于this._value === null || this._value === undefined
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    你看下上面的代码,是不是健壮性就好一点了呢?

    Either 函子

    在MayBe函子中,很难确认哪一步产生的空值问题。所以就有了Either

    class Left {
        static of(value) {
            return new Left(value)
        }
    
        constructor(value) {
            this._value = value
        }
    
        map(fn) {
            return this
        }
    }
    
    class Right {
        static of(value) {
            return new Right(value)
        }
    
        constructor(value) {
            this._value = value
        }
    
        map(fn) {
            return Right.of(fn(this._value))
        }
    }
    
    function parseJSON(str) {
        try {
            return Right.of(JSON.parse(str))
        } catch (e) {
            return Left.of({ error: e.message })
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    写在最后

    开篇明义,函数编程范式,既然是一种编程风格,就意味着掌握它并非单单只是看看这篇文章就够了,需要你日积月累的努力,并在日常代码编写中下意识地去使用它们。

  • 相关阅读:
    大学时光仅四年,疫情反反复复占几年
    iOS开发之弹窗管理
    【招招制敌】修改element-ui中el-image 预览图大小的默认尺寸,让展示效果更加有呼吸感
    存储服务器特征是什么
    Linux编译器-gcc/g++使用
    HT878T 可任意限幅、内置自适应升压的音频功放应用于哪些领域
    Nanoprobes Alexa Fluor 488 FluoroNanogold 偶联物
    【从跳板机ssh到内网目标服务器】配置vscode实现远程连接
    uniapp 微信小程序之隐私协议开发
    PHP7 +nginx Docker 部署
  • 原文地址:https://blog.csdn.net/weixin_49172439/article/details/126649192