zoukankan      html  css  js  c++  java
  • 浅析js中的纯函数、高阶函数、记忆函数、偏函数

    前言

    上周分享文档中遇到几个关键名称,纯函数、高阶函数、记忆函数、偏函数....,这里做一下解析与举例

     

     

    纯函数

    简介

    纯函数是函数式编程中非常重要的一个概念,简单来说,就是一个函数的返回结果只依赖于它的参数,并且在执行过程中没有副作用,我们就把这个函数叫做纯函数

     

    定义

    一个函数,如果符合以下两个特点,那么它就可以称之为纯函数

    • 对于相同的输入,永远得到相同的输出(函数的返回结果只依赖于它的参数)

    • 没有任何可观察到的副作用(函数执行过程中没有副作用)

     

    解析--函数的返回结果只依赖于它的参数

    let greeting = 'Hello'
    
    function greet (name) {
      return greeting + ' ' + name
    }
    
    console.log(greet('World'))  // Hello World

    上面代码中,greet函数不是一个纯函数,因为它的返回结果依赖外部变量greeting,因为greeting是有可能变化的,所以我们不能保证greet('World')的值永远是Hello World。虽然greet函数的代码没有变化,传入的参数也没有变化,但它的返回值是不可预料的

    接着做以下修改

    function greet (greeting, name) {
      return greeting + ' ' + name
    }
    
    console.log(greet('Hi', 'Savo'))   // Hi Savo
    

    现在,greet的返回结果只依赖于它的参数greeting和name,就是说,只要代码不变,greet('Hi', 'Savo')的返回值永远是Hi Savo

    这就是纯函数的第一个条件:函数的返回结果只依赖于它的参数

    解析--没有副作用

    没有副作用的意思是,这个函数的运行,不会修改外部的状态,即不会污染外部作用域

    const user = {
      name: 'Nick Brown'
    }
    
    let flag = false
    
    function checkData (user) {
      if (user.name.length > 4) {
        flag = true
      }
    }
    

    可见,执行函数的时候会修改到 flag 的值(注意:如果函数没有任何返回值,那么它很可能就具有副作用)

    由于定义的函数改变的值在函数作用域之外,导致这个函数成为“不纯”的函数

    const user = {
      name: 'Nick Brown'
    }
    
    function checkData (user) {
      return user.name.length > 4
    }
    
    const flag = checkData(user)
    

    上面的代码,我们只计算了作用域内的局部变量,没有任何作用域外部的变量被改变,因此这个函数是“纯函数”

    除了修改外部的变量,一个函数在执行过程中还有很多方式产生外部可观察的变化,比如说调用 DOM API 修改页面,或者你发送了 Ajax 请求,还有调用 window.reload刷新浏览器,甚至是 console.log 往控制台打印数据也是副作用

    纯函数很严格,也就是说你几乎除了计算数据以外什么都不能干,计算的时候还不能依赖除了函数参数以外的数据

    特殊例子

    再来看看JavaScript中常用的两个方法slice和splice

    const arr1 = [0, 1, 2, 3, 4, 5, 6]
    const arr2 = [0, 1, 2, 3, 4, 5 ,6]
    
    const spliceArr = arr1.splice(0, 2)
    const sliceArr = arr2.slice(0, 2)
    
    console.log('arr1: ', arr1)
    console.log('spliceArray: ', spliceArr)
    
    console.log('arr2: ', arr2)
    console.log('sliceArray: ', sliceArr)
    
    // 执行结果
    array1: 2,3,4,5,6
    spliceArray: 0,1
    array2: 0,1,2,3,4,5,6
    sliceArray: 0,1
    

    可以看到,slice和splice的作用是大致相同的,但是splice改变了原数组,而slice却没有,实际开发中,slice这种不改变原数组的方式更安全一些,改变原始数组,是一种副作用

    场景

    纯函数可以根据输入来做缓存,实现缓存的是一种叫做 memorize 的技术,例Vue 源码

    /**
     * Create a cached version of a pure function.
     * 只适用于缓存 接收一个字符串为参数的 fn
     */
    export function cached (fn) {
      const cache = Object.create(null)
      return function cachedFn (str) {
        const hit = cache[str]
        return hit || (cache[str] = fn(str))
      }
    }
    
    /**
     * Capitalize a string.
     */
    export const capitalize = cached((str) => {
      return str.charAt(0).toUpperCase() + str.slice(1)
    })
    

    优点

    • 可复用性/可移植性

      纯函数仅依赖于传入的参数,这意味着你可以随意将这个函数移植到别的代码中,只需要提供踏需要的参数即可。如果是非纯函数,有可能你需要一根香蕉,却需要将整个香蕉树搬过去

    • 可测试性

      纯函数非常容易进行单元测试,因为不需要考虑上下文环境,只需要考虑输入和输出

    • 并行代码

      纯函数是健壮的,改变执行次序不会对系统造成影响,因此纯函数的操作可以并行执行

    高阶函数

    简介

    英文叫Higher-order function。JavaScript的函数其实都指向某个变量。既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数

     

    特点

    • 函数可以作为参数传递(接受一个或多个函数作为输入)

    • 函数可以作为返回值输出(输出一个函数)

     

    解析

    可以将高阶函数理解为函数之上的函数,它很常用,比如常见的回调函数

    • 在ajax异步请求的过程中,回调函数使用的非常频繁

    • 在不确定请求返回的时间时,将callback回调函数当成参数传入

    • 待请求完成后执行callback函数

    const getData = function(url, callback) {
        ajax.get(url, function(data){
            callback(data)
        })
    }
    

    或者在众多闭包的场景中都使用到

    比如 防抖函数(debounce)与节流函数(throttle)

     

    防抖函数

    防抖,指的是无论某个动作被连续触发多少次,直到这个连续动作停止一定到延时后,才会被当作一次来执行,即多合一

    比如一个输入框接受用户不断输入,输入结束后才开始搜索

    以页面滚动作为例子,可以定义一个防抖函数,接受一个自定义的 delay值,作为判断停止的时间标识

    // 这个是用来获取当前时间戳的
    function now() {
      return +new Date()
    }
    /**
     * 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
     *
     * @param  {function} func        回调函数
     * @param  {number}   wait        表示时间窗口的间隔
     * @param  {boolean}  immediate   设置为ture时,是否立即调用函数
     * @return {function}             返回客户调用函数
     */
    function debounce (func, wait = 50, immediate = true) {
      let timer, context, args
    
      // 延迟执行函数
      const later = () => setTimeout(() => {
        // 延迟函数执行完毕,清空缓存的定时器序号
        timer = null
        // 延迟执行的情况下,函数会在延迟函数中执行
        // 使用到之前缓存的参数和上下文
        if (!immediate) {
          func.apply(context, args)
          context = args = null
        }
      }, wait)
    
      // 这里返回的函数是每次实际调用的函数
      return function(...params) {
        // 如果没有创建延迟执行函数(later),就创建一个
        if (!timer) {
          timer = later()
          // 如果是立即执行,调用函数
          // 否则缓存参数和调用上下文
          if (immediate) {
            func.apply(this, params)
          } else {
            context = this
            args = params
          }
        // 如果已有延迟执行函数(later),调用的时候清除原来的并重新设定一个
        // 这样做延迟函数会重新计时
        } else {
          clearTimeout(timer)
          timer = later()
        }
      }
    }
    

    记忆函数

    简介

    记忆化(memoization)是一种构建函数的处理过程,能够记住上次计算结果的函数。在实现中,可以这样进行处理:当函数计算得到结果时,就将该结果按照参数存储起来。采取这种方式时,如果另外一个调用也使用相同的参数,我们则可以直接返回上次存储的结果而不是再计算一遍

    显而易见,像这样避免既重复又复杂的计算可以显著提高性能。对于动画中的计算、搜索不经常变化的数据或任何耗时的数学计算来说,记忆化这种方式是十分有用的

     

    实例分析

    // 自记忆素数检测函数
    function isPrime (value) {
      // 创建缓存
      if (!isPrime.answers) {
        isPrime.answers = {};
      }
      // 检查缓存的值
      if (isPrime.answers[value] !== undefined) {
        return isPrime.answers[value];
      }
      // 0和1不是素数
      var prime = value !== 0 && value !== 1;
      // 检查是否为素数
      for (var i = 2; i < value; i++) {
        if (value % i === 0) {
          prime = false;
          break;
        }
      }
      // 存储计算值
      return isPrime.answers[value] = prime
    }
    

    isPrime函数是一个自记忆素数检测函数,每当它被调用时

    • 首先,检查它的answers属性来确认是否已经有自记忆的缓存,如果没有,创建一个

    • 接下来,检查参数之前是否已经被缓存过,如果在缓存中找到该值,直接返回缓存的结果

    • 如果参数是一个全新的值,进行正常的素数检测

    • 最后,存储并返回计算值

    优缺点

    优点

    • 由于函数调用时会寻找之前调用所得到的值,所以用户最终会乐于看到所获得的性能收益

    • 它不需要执行任何特殊请求,也不需要做任何额外初始化,就能顺利进行工作

    缺陷

    • 任何类型的缓存都必然会为性能牺牲内存

    • 缓存逻辑不应该和业务逻辑混合,一个方法只需要把一件事情做好

    • 对记忆函数很难做负载测试或估算算法复杂度,因为结果依赖于函数之前的输入

     

     

    偏函数

    简介

    偏函数属于函数式编程的一部分,使用偏函数可以通过有效地“冻结”那些预先确定的参数,来缓存函数参数,然后在运行时,当获得需要的剩余参数后,可以将他们解冻,传递到最终的参数中,从而使用最终确定的所有参数去调用函数

    简而言之,偏函数是 JS 函数柯里化运算的一种特定应用场景,就是把一个函数的某些参数先固化,也就是设置默认值,返回一个新的函数,在新函数中继续接收剩余参数,这样调用这个新函数会更简单

     

    常见场景试例

    将一些函数组合封装到一个函数中,调用时可以按顺序实现全部功能

    function toUpperCase(str) {
        return str.toUpperCase() // 将字符串变成大写
    }
    
    function add(str) {
        return str + '!!!' // 将字符串拼接
    }
    
    function split(str) {
        return str.split('') // 将字符串拆分为数组
    }
    
    function reverse(arr) {
        return arr.reverse() // 将数组逆序
    }
    
    function join(arr) {
        return arr.join('-') // 将数组按'-'拼接成字符串
    }
    
    function compose() {
        const args = Array.prototype.slice.call(arguments)  // 类数组转换为数组
        const len = args.length - 1  // 最后一个参数的索引
        return function(x) {
            let result = args[len](x)  // 执行最后一个函数的结果
            while(len--) {
                result = args[len](result)  // 执行每个函数的结果
            }
            return result 
        }
    }
    
    const f = compose(add, join, reverse, split, toUpperCase) 
    console.log( f('cba') )  // A-B-C!!!
    

    在组合函数 compose 中,依次执行 toUpperCase、split、reverse、join、add 实现全部功能

    当然有更优雅的写法,通过数组自带的方法实现

    const compose2 = (...args) => x => args.reduceRight((result, cb) => cb(res), x) 
    
    const f = compose2(add, join, reverse, split, toUpperCase) 
    console.log( f('cba') )   // A-B-C!!!
    
  • 相关阅读:
    Cuckoo for Hashing_双哈希表
    nyoj113_字符串替换
    nyoj366_D的小L_字典序_全排列
    二叉树的前序 中序 后序 遍历(递归/非递归)
    Java 学习路线
    leetcode 04 Median of Two Sorted Arrays
    ThreadLocal 的机制与内存泄漏
    try finally 执行顺序问题
    Java中的类加载器
    快速理解Java中的七种单例模式
  • 原文地址:https://www.cnblogs.com/zhazhanitian/p/13063999.html
Copyright © 2011-2022 走看看