浅析js中的纯函数、高阶函数、记忆函数、偏函数

更新日期: 2020-05-08阅读: 1.4k标签: 函数

前言

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

 

纯函数

简介

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

定义

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

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

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

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

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

链接: https://www.fly63.com/article/detial/9275

JavaScript 函数式编程

我理解的 JavaScript 函数式编程,都认为属于函数式编程的范畴,只要他们是以函数作为主要载体的。

Js函数式编程,给你的代码增加一点点函数式编程的特性

给你的代码增加一点点函数式编程的特性,最近我对函数式编程非常感兴趣。这个概念让我着迷:应用数学来增强抽象性和强制纯粹性,以避免副作用,并实现代码的良好可复用性。同时,函数式编程非常复杂。

让我们来创建一个JavaScript Wait函数

Async/await以及它底层promises的应用正在猛烈地冲击着JS的世界。在大多数客户端和JS服务端平台的支持下,回调编程已经成为过去的事情。当然,基于回调的编程很丑陋的。

JavaScript函数创建的细节

如果你曾经了解或编写过JavaScript,你可能已经注意到定义函数的方法有两种。即便是对编程语言有更多经验的人也很难理解这些差异。在这篇博客的第一部分,我们将深入探讨函数声明和函数表达式之间的差异。

编写小而美函数的艺术

随着软件应用的复杂度不断上升,为了确保应用稳定且易拓展,代码质量就变的越来越重要。不幸的是,包括我在内的几乎每个开发者在职业生涯中都会面对质量很差的代码。这些代码通常有以下特征:

javascript回调函数的理解和使用方法(callback)

在js开发中,程序代码是从上而下一条线执行的,但有时候我们需要等待一个操作结束后,再进行下一步操作,这个时候就需要用到回调函数。 在js中,函数也是对象,确切地说:函数是用Function()构造函数创建的Function对象。

js调用函数的几种方法_ES5/ES6的函数调用方式

这篇文章主要介绍ES5中函数的4种调用,在ES5中函数内容的this指向和调用方法有关。以及ES6中函数的调用,使用箭头函数,其中箭头函数的this是和定义时有关和调用无关。

JavaScript中函数的三种定义方法

函数的三种定义方法分别是:函数定义语句、函数直接量表达式和Function()构造函数的方法,下面依次介绍这几种方法具体怎么实现,在实际编程中,Function()构造函数很少用到,前两中定义方法使用比较普遍。

js在excel的编写_excel支持使用JavaScript自定义函数编写

微软 称excel就实现面向开发者的功能,也就是说我们不仅可以全新定义的公式,还可以重新定义excel的内置函数,现在Excel自定义函数增加了使用 JavaScript 编写的支持,下面就简单介绍下如何使用js来编写excel自定义函数。

js中的立即执行函数的写法,立即执行函数作用是什么?

这篇文章主要讲解:js立即执行函数是什么?js使用立即执行函数有什么作用呢?js立即执行函数的写法有哪些?

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!