zoukankan      html  css  js  c++  java
  • 柯里化与反柯里化

    前言

    在 JavaScript 中,柯里化和反柯里化是高阶函数的一种应用。在这之前简要介绍一下什么是高阶函数,通俗的说,函数可以作为参数传递到函数中,这个作为参数的函数叫回调函数,而拥有这个参数的函数就是高阶函数,回调函数在高阶函数中调用并传递相应的参数,在高阶函数执行时,由于回调函数的内部逻辑不同,高阶函数的执行结果也不同,非常灵活,也被叫做函数式编程

     

     

    柯里化

    定义

    在计算机科学中,柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数并且返回接受余下参数且返回结果的新函数的技术

    简单的来说可以理解为提前接收部分参数,延迟执行,不立即输出结果,而是返回一个接受剩余参数的函数

    因为这样的特性,也被称为部分计算函数。柯里化,是一个逐步接收参数的过程

     

    高阶函数

    高阶函数是实现柯里化的基础,高阶函数是至少满足以下两个特性之一

    1. 函数可以作为参数被传递

    2. 函数可以作为返回值输出

    式例

    场景:程序员每天加班的时间还是比较多的,现在我们需要计算一个程序员每天的加班时间,常规反应应该是这样

    let overtime = 0
    
    const time = x => {
     return overtime += x
    }
    
    time(1)   // 1
    time(2)   // 3
    time(3)   // 6
    

    上面的代码固然没有问题,可是需要每天调用都算加一下当天的时间,很麻烦,并且每调用一次函数都要进行一定的操作,如果数据量巨大,有可能会有影响性能的风险

    基次进行初步改造

    const time = x => {
      return y => {
        return x + y
      } 
    }
    
    const times = time(0)
    times(3)
    

    但是上面代码依然存在问题,在实际开发中很多时候我们的参数是不确定的,上面代码虽然简单的实现了柯里化的基本操作,但是对于参数不确定的情况是处理不了的,所以存在着函数参数的局限性,不过我们从上面的代码中基本可以知道函数柯里化是个啥意思了;就是一个函数调用的时候只允许传入一个参数,然后通过闭包返回内部函数去处理和接收剩余参数,返回的函数通过闭包的方式记住了time的第一个参数

    接着对代码改造

    // 首先定义一个变量接收函数
    const overtime = (function() {
      // 定义一个数组用来接收参数
      const args = []
      //这里运用闭包,调用外部函数返回一个内部函数
      return function() {
        //arguments是浏览器内置对象,专门用来接收参数
        //如果参数的长度为0即没有参数的时候
        if(arguments.length === 0) {
          //定义变量用来累加
          let time = 0
          //循环累加,用i和args的长度进行比较
          for (const i = 0, l = args.length; i < l; i++) {
            //进行累加操作 等价于time = time + args[i]
            time += args[i]
          }
          // 返回累加的结果
          return time
          //如果arguments对象参数长度不为零,即有参数的时候
        }else {
          //定义的空数组添加arguments参数作为数组项,第一个参数古args作为改变this指向,第二个参数arguments把剩余参数作为数组形式添加至空数组中
          [].push.apply(args, arguments)
        }
      }
    })()
    
    overtime(3.5)    // 第一天
    overtime(4.5)    // 第二天
    overtime(2.1)    // 第三天
    
    console.log( overtime() ) // 10.1
    

    代码经过改造已经实现了功能,但是这不是一个函数柯里化的完整实现,说白了就是不通用

    接着对代码改造

    // 定义方法currying,先传入一个参数
    const currying = fn => {
      // 定义空数组装arguments对象的剩余参数
      const args = []
      // 利用闭包返回一个函数处理剩余参数
      return function () {
        // 如果arguments的参数长度为0,即没有剩余参数
        if(arguments.length === 0) {
          // 执行上面方法
          return fn.apply(this, args)
        }
        console.log(arguments)
        // 如果arguments的参数长度不为0,即还有剩余参数
        // 在数组的原型对象上添加数组,apply用来更改this的指向为args
        // 将[].slice.call(arguments)的数组添加到原型数组上
        Array.prototype.push.apply(args, [].slice.call(arguments))
        // args.push([].slice.call(arguments))
        // 这里返回的arguments.callee是返回的闭包函数,callee是arguments对象里面的一个属性,用于返回正被执行的function对象
        return arguments.callee
      }
    }
    
    // 这里调用currying方法并传入add函数,结果会返回闭包内部函数
    cosnt s = currying(add)
    
    // 调用闭包内部函数,当有参数的时候会将参数逐步添加到args数组中,待没有参数传入的时候直接调用
    // 调用的时候支持链式操作
    s(1)(2)(3)()
    console.log(s())
    
    //也可以一次性传入多个参数
    s(1, 2, 3)
    console.log(s())
    

    函数柯里化的优点

    1. 可以延迟计算,即如果调用柯里化函数传入参数是不调用的,会将参数添加到数组中存储,等到没有参数传入的时候进行调用

    2. 参数复用,当在多次调用同一个函数,并且传递的参数绝大多数是相同的,那么该函数可能是一个很好的柯里化候选

     

    小结

    柯里化,在这个例子中可以看出很明显的行为规范

    • 逐步接收参数,并缓存供后期计算使用

    • 不立即计算,延后执行

    • 符合计算的条件,将缓存的参数,统一传递给执行方法

     

     

    反柯里化

    定义

    从字面意思上来讲就是跟柯里化的意思相反,其实真正的反柯里化的作用是扩大适用范围,就是说当我们调用某个方法的时候,不需要考虑这个对象自身在设计的过程中有没有这个方法,只要这个方法适用于它,我们就可以使用

    反柯里化,是一个泛型化的过程。它使得被反柯里化的函数,可以接收更多参数。目的是创建一个更普适性的函数,可以被不同的对象使用。有鸠占鹊巢的效果

     

    鸭子类型思想

    在学习JS反柯里化之前,我们先学习一下动态语言的鸭子类型思想,以助于我们更好的理解

    在程序设计中,鸭子类型(duck typing)是动态类型的一种风格

    在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由当前方法和属性的集合决定

    这个概念的名字来源于由 James Whitcomb Riley 提出的鸭子测试,“鸭子测试”可以这样表述

    当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子

    理论上的解释往往干涩难懂,换成人话来说就是:你是你妈妈的儿子/女儿,不管你是否优秀,是否漂亮,只要你是你妈亲生的,那么你就是你妈的儿子/女儿;换成鸭子类型就是,只要你会呱呱叫,走起来像鸭子,只要你拥有的行为像鸭子,不管你是不是鸭子,那么你就可以被称为鸭子

    在Javascript中有很多鸭子类型的引用,比如我们在对一个变量进行赋值的时候,显然是不需要考虑对象的类型的,正是因为如此,Javascript才更加的灵活,所以Javascript是一门典型的动态类型语言

    接着来看一下反柯里化中是怎么引用鸭子类型的

    // 函数原型对象上添加uncurring方法
    Function.prototype.uncurring = function() {
      // 改变this的指向 
      // 这里的this指向是Array.prototype.push
      const self = this
      // 这里的闭包用来返回内部函数的执行
      return function() {
        // 创建一个变量,在数组的原型对象上添加shift上面删除第一个参数
        // 改变数组this的指向为arguments
        const obj = Array.prototype.shift.call(arguments)
        // 最后返回执行并给方法改变指向为obj也就是arguments
        // 并传入arguments作为参数
        return self.apply(obj, arguments)
      }
    }
    
    // 数组原型对象上添加uncurrying方法
    const push = Array.prototype.push.uncurring()
    // 测试一下
    // 匿名函数自执行
    (function() {
      // 这里的push就是一个函数方法了
      // 相当于传入参数arguments和4两个参数,但是在上面shift方法中删除第一个参数,这里的arguments参数被截取了,所以最后实际上只传入了4
      push(arguments, 4)
      console.log(arguments) // [1, 2, 3, 4]
      // 匿名函数自调用并带入参数1,2,3
    })(1, 2, 3)
    

    说到这里联想到一个问题,arguments是一个接收参数的对象,里面是没有push方法的,那么arguments为什么能调用push方法呢

    答案因为代码 var push = Array.prototype.push.uncurring() 在数组的原型对象的push方法上添加了uncurring方法,然后在执行匿名函数的方法 push(arguments, 4) 时候实质上是在调用上面的方法在Function的原型对象上添加uncurring方法并返回一个闭包内部函数执行,在执行的过程中因为Array原型对象上的shift方法会把 push(arguments, 4) 中的arguments截取,所以其实方法的实际调用是push(4),所以最终的结果才是[1, 2, 3, 4]

     

    场景

    •  判断变量类型(反柯里化)

      const fn = function(){}
      const val = 1
      
      if(Object.prototype.toString.call(fn) == '[object Function]') {
        console.log(`${fn} is function.`)
      }
      
      if(Object.prototype.toString.call(val) == '[object Number]') {
        console.log(`${val} is number.`)
      }
      

      用反柯里化实现

      const fn = function(){}
      const val = 1
      const toString = Object.prototype.toString.unCurrying()
      
      if(toString(fn) == '[object Function]') {
        console.log(`${fn} is function.`)
      }
      
      if(toString(val) == '[object Number]') {
        console.log(`${val} is number.`)
      }
      
    • 监听事件(柯里化)
      function nodeListen(node, eventName) {
        return function(fn) {
          node.addEventListener(eventName, function() {
            fn.apply(this, Array.prototype.slice.call(arguments))
          }, false)
        }
      }
      
      const bodyClickListen = nodeListen(document.body, 'click')
      bodyClickListen(function() {
        console.log('first listen')
      })
      
      bodyClickListen(function() {
        console.log('second listen')
      })
      

      使用柯里化,优化监听DOM节点事件,addEventListener三个参数不用每次都写

  • 相关阅读:
    leetcode 之Binary Tree Postorder Traversal
    关于java中this的一些总结
    Javascript的匿名函数与自执行
    js 闭包学习笔记
    滚动条到底自动加载数据
    AMD:异步模块定义
    Sass、LESS 和 Stylus
    【原创】Mysql设置自增长主键的初始值
    -webkit-animation的使用
    CSS滤镜
  • 原文地址:https://www.cnblogs.com/zhazhanitian/p/14355149.html
Copyright © 2011-2022 走看看