JavaScript中的函数式编程--纯函数

166 阅读4分钟

纯函数

  • 纯函数:相同的输入永远会得到相同的输出,而且没有任何可观察 的副作用
  • 类比:纯函数就类似数学中的函数(用来描述输入和输出之间的关系,也可以称作为映射,如:y = f(x))

如下图所示:左侧代表输入,右侧代表输出,f标识输入和输出之间的关系

avatar

  • lodash是一个纯函数的功能库,提供了对数组、数字、字符串、函数等操作的一些方法
  • 其中数组的slice是纯函数,splice不是纯函数
    • slice 返回数组中的指定部分,不会改变原数组
    • splice 对数组进行操作后返回数组,会改变原数组

slice和splice示例:

    // slice
    let nums = [1, 2, 3, 4, 5]
    // 多次调用slice(start, end)  不包括end位
    nums.slice(0, 3) // => [1, 2, 3]
    nums.slice(0, 3) // => [1, 2, 3]
    nums.slice(0, 3) // => [1, 2, 3]
    // 在多次调用时相同的输入得到相同的输出
    // splice
    let nums = [1, 2, 3, 4, 5]
    // 多次调用slice(start, length)
    nums.splice(0, 3) // => [1, 2, 3]
    nums.splice(0, 3) // => [4, 5]
    nums.splice(0, 3) // => []
    // 在多次调用时相同的输入得到不同的输出
  • 函数式编程不会保留计算的中间结果,所以变量是不可变的(无状态的)
  • 我们可以把一个函数的执行结果交给另一个函数去处理

纯函数的好处

  • 可缓存
    • 纯函数对相同的输入始终有相同的输出,所以可以把纯函数的结果缓存起来
  • 可测试
    • 纯函数让测试更方便(输入对应输出,单元测试就是断言输出结果)
  • 并行处理
    • 在多线程环境下并行操作共享内存数据很可能会出现意外情况(如:同时修改全局变量)
    • 纯函数不需要访问共享的数据内存,所以在并行环境下可以任意运行纯函数(ES6新增Web Worker可以开启多线程)

示例:lodash中的记忆函数memoize

    // 求圆的面积
    const _ = require('lodash')
    // 创建求圆面积的纯函数
    function getArea(r) {
        // 为了验证该函数被执行几次
        console.log(r)
        return Math.PI * r * r
    }
    // memoize会返回一个带记忆功能的函数
    let getAreaWithMemory = _.memoize(getArea)
    // 执行多次getAreaWithMemory
    console.log(getAreaWithMemory(2))
    console.log(getAreaWithMemory(2))
    console.log(getAreaWithMemory(2))
    // 输出结果
    // 2
    // 12.566368
    // 12.566368
    // 12.566368
    // 此时我们发现getArea函数内部的log只输出了1次

示例:模拟lodash中memoize函数实现

    // memoize接收一个函数,返回值也是一个函数
    function memoize(fn) {
        // 定义一个对象把函数的执行结果缓存起来
        // fn为一个纯函数,相同的输入对应相同的输出
        // 因此对象中key为输入数据,value为函数执行结果
        let cache = {}
        return function() {
            // 执行函数时首先判断cache中是否已缓存
            // 已缓存:不再去计算直接从cahce中取
            // 未缓存:计算后将结果缓存起来
            let key = JSON.stringify(arguments) // 将伪数组arguments转换成string
            // 判断cache中是否已有缓存
            // apply方法可以改变函数内部this,也可以将参数展开
            cache[key] = cache[key] || fn.apply(fn, arguments)
            return cache[key]
        }
    }
    // 测试
    let getAreaWithMemory = memoize(getArea)
    // 执行多次getAreaWithMemory
    console.log(getAreaWithMemory(2))
    console.log(getAreaWithMemory(2))
    console.log(getAreaWithMemory(2))

纯函数的副作用

  • 纯函数:对于相同的输入永远会得到相同的输出,而且没有任何可观察的副作用

示例:年龄判断

    // 非纯函数
    let mini = 18 // mini为全局变量,当发生变化时不满足纯函数第一条
    function checkAge(age) {
        return age >= mini
    }
    
    // 纯函数(有硬编码,后边可通过函数的柯里化解决)
    function checkAge(age) {
        let mini = 18 // 编成局部变量
        return age >= mini
    }
  • 副作用:让一个函数变得不纯(相同输入不能保证相同输出),如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用
  • 副作用来源:
    • 全局变量
    • 配置文件
    • 数据库
    • 获取用户的输入
    • ......

所有的外部交互都有可能带来副作用,副作用也使得方法的通用性降低不适合扩展和可重用性,同事副作用会给程序带来安全隐患(跨站脚本攻击),但是副作用不可能完全禁止,尽可能控制他们在可控范围内发生