浅析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!!!

 

posted @ 2020-06-08 10:22  渣渣逆天  阅读(934)  评论(0编辑  收藏  举报