zoukankan      html  css  js  c++  java
  • JavaScript中的函数柯里化与反柯里化

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

    高阶函数
    高阶函数是实现柯里化的基础,高阶函数是至少满足以下两个特性之一
    1、函数可以作为参数被传递
    2、函数可以作为返回值输出
     

    二、柯里化通用实现方式
    第一种
    满足原始函数的参数个数即可以执行
    1、递归写法(比较绕,但是可操作性更强,可以在继续下一轮参数收集前做其他处理)
    // 递归写法(比较绕,但是可操作性更强,可以在继续下一轮参数收集前做其他处理)
    function curry(fn, ...params) {
        let _args = params || []   // 提前传递的部分参数
        let len = fn.length         // 原函数的参数个数
        return (...rest) => {
            Array.prototype.push.call(_args, ...rest)
            console.log('_args :', _args, ', rest :', ...rest)
            if (_args.length >= len) { // 收集到的参数大于等于原始函数的参数数量,则执行原函数
                // 置空之后,柯里化后的函数可以在满足调用条件之后,继续开始新一轮的参数收集,
                // 否则该函数在第一次满足参数收集后,之后的调用都是返回第一次收集完参数调用的结果
                /**
                 * 不置空
                 */
                // return fn.apply(this, _args) // 收集到的参数满足原函数参数个数,则执行原函数
                 /**
                  * 置空
                  */
                let _newArgs = Array.from(_args)
                _args = []
                return fn.apply(this, _newArgs)
            }
            return curry.call(this, fn, ..._args)
        }
    }

    2、具名函数写法(更浅显易懂,明确的返回具名函数

    // 具名函数写法(更浅显易懂,明确的返回具名函数)
    function curry(fn, ...params) {
        let _args = params || []
        let len = fn.length
        return function _fn(...rest) {
            Array.prototype.push.call(_args, ...rest)
            console.log('_args :', _args, ', rest :', ...rest)
            if (_args.length >= len) {
                /**
                 * 不置空
                 */
                // return fn.apply(this, _args)
                /**
                 * 置空
                 */
                let _newArgs = Array.from(_args)
                _args = []
                return fn.apply(this, _newArgs)
            }
            return _fn
        }
    }

    例子

    // 输出日志函数
    // 柯里化后,收集完所有参数后,才执行,只被执行一次
    function log(sec, min, hour) {
        console.log('sec, min, hour: ', sec, min, hour)
    }
    let curryLog = curry(log) // _args : []
    
    curryLog('3s') // _args : [ '3s' ] , rest : 3s --- 未收集满原函数参数个数,即不满足 _args.length >= len 条件,递归执行 curry 函数/ 返回具名函数
    curryLog('8m') // _args : [ '3s', '8m' ] , rest : 8m --- 未收集满原函数参数个数,即不满足 _args.length >= len 条件,递归执行 curry 函数/ 返回具名函数
    curryLog('0h') 
    // _args : [ '3s', '8m', '0h' ] , rest : 0h
    // sec, min, hour:  3s 8m 0h -- 收集满参数(这里参数有三个),执行原函数
    上面是收集满参数个数就执行原函数,

    这里有个注意点, _args = [] 是否要置为空
    置空之后,可以继续开始新一轮的参数收集
    否则该函数在第一次满足参数收集,调用原函数,之后的再次调用,会一直返回第一次收集满参数后调用原函数的结果
    因为 _args 每次调用都被重新赋值为之前收集的参数数组,那么以后的每次调用
    它的个数都是超过原有函数的个数,会一直满足调用条件,
    _args 的值一直在递增收集,但是原函数所接受的我们设定的参数是有限的,
    那么超过原函数所接受的参数,接受的参数值一直是 _args 的前几个, 这样原函数调用结果都是一样的

    看看置空与不置空的情况

    不置空,接上面代码执行下去

    _args = [] // 不置空
    curryLog('5s') 
    // sec, min, hour:  3s 8m 0h
    // _args : [ '3s', '8m', '0h', '5s' ] , rest : 5s
    
    curryLog('6h') 
    // _args : [ '3s', '8m', '0h', '5s', '6h' ] , rest : 6h
    // sec, min, hour:  3s 8m 0h
    
    curryLog('1h') 
    // _args : [ '3s', '8m', '0h', '5s', '6h', '1h' ] , rest : 1h
    // sec, min, hour:  3s 8m 0h

    置空,接上面代码执行下去

    // _args = [] // 置空(支持新开一轮收集)
    curryLog('5s') // _args : [ '5s' ] , rest : 5s
    curryLog('6h') // _args : [ '5s', '6h' ] , rest : 6h
    curryLog('1h') 
    // _args : [ '5s', '6h', '1h' ] , rest : 1h
    // sec, min, hour:  5s 6h 1h

    第二种

    可以自己控制最后执行时机
    1、递归写法(比较绕,但是可操作性更强,可以在继续下一轮参数收集前做其他处理)
    // 递归写法(比较绕,但是可操作性更强,可以在继续下了一轮参数收集前做其他处理)
    function curry(fn, ...params) {
        let _args = params || []
        return (...rest) => {
            console.log('_args :', _args, ', rest :', ...rest)
            if (rest.length === 0) { // 与上面的差别在于条件判断,只要传的参数为空,即执行原函数
                // 是否需要置空,与上面分析情况一样
                /**
                 * 不置空
                 */
                // return fn.apply(this, _args)
                /**
                 * 置空
                 */
                let _newArgs = Array.from(_args)
                _args = []
                return fn.apply(this, _newArgs)
            }        
            Array.prototype.push.call(_args, ...rest) // 自己控制最后执行时机,当前语句放于 if 判断之后,减少执行
            return curry.call(this, fn, ..._args)
        }
    }
     
    2、具名函数写法(更浅显易懂,明确的返回具名函数
    // 具名函数写法(更浅显易懂,明确的返回具名函数)
    function curry(fn, ...params) {
        let _args = params || []
        return function _fn(...rest) { // 此处使用具名函数,用于 return,这么做逻辑更清晰;就不用像上面注释的那样,递归调用 curry 函数
            console.log('_args :', _args, ', rest :', ...rest)
            if (rest.length === 0) {
                /**
                 * 不置空
                 */
                // return fn.apply(this, _args)
                /**
                 * 置空
                 */
                let _newArgs = Array.from(_args)
                _args = []
                return fn.apply(this, _newArgs)
            }
            Array.prototype.push.call(_args, ...rest) // 自己控制最后执行时机,当前语句放于 if 判断之后,减少执行
            return _fn
        }
    }

    例子

    // 输出日志函数
    // 柯里化后,收集完所有参数后,才执行,只被执行一次
    function log(sec, min, hour) {
        console.log('sec, min, hour: ', sec, min, hour)
    }
    let curryLog = curry(log)
    
    curryLog('3s')  // _args : [] , rest : 3s
    curryLog('8m')  // _args : [ '3s' ] , rest : 8m
    curryLog('0h')  // _args : [ '3s', '8m' ] , rest : 0h
    
    curryLog('5s')  // _args : [ '3s', '8m', '0h' ] , rest : 5s
    curryLog('6h')  // _args : [ '3s', '8m', '0h', '5s' ] , rest : 6h
    curryLog('1h')  // _args : [ '3s', '8m', '0h', '5s', '6h' ] , rest : 1h
    curryLog()      
    // _args : [ '3s', '8m', '0h', '5s', '6h', '1h' ] , rest :
    // sec, min, hour:  3s 8m 0h --- 传入参数为空,满足执行条件,只取前三个参数
    上面满足条件执行后,亦存在是否让 _args = [] 的问题
     

    看看置空与不置空的情况

    不置空,接上面代码执行下去

    // _args = []   // 不置空
    curryLog('5s')  // _args : [ '3s', '8m', '0h', '5s', '6h', '1h' ] , rest : 5s
    curryLog('6h')  // _args : [ '3s', '8m', '0h', '5s', '6h', '1h', '5s' ] , rest : 6h
    curryLog('1h')  // _args : [ '3s', '8m', '0h', '5s', '6h', '1h', '5s', '6h' ] , rest : 1h
    curryLog()      
    // _args : [ '3s', '8m', '0h', '5s', '6h', '1h', '5s', '6h', '1h' ] , rest :
    // sec, min, hour:  3s 8m 0h --- 传入参数为空,满足执行条件,只取前三个参数
    置空,接上面代码执行下去
    // _args = [] // 置空(支持新开一轮收集)
    curryLog('5s')  // _args : [] , rest : 5s
    curryLog('6h')  // _args : [ '5s' ] , rest : 6h
    curryLog('1h')  // _args : [ '5s', '6h' ] , rest : 1h
    curryLog()      
    // _args : [ '5s', '6h', '1h' ] , rest :
    // sec, min, hour:  5s 6h 1h --- 传入参数为空,满足执行条件,只取前三个参数

    三、应用场景


    函数柯里化有哪些用处呢?
    一、可以惰性求值
    二、可以提前传递部分参数
     
    1、惰性求值
    // 计算月度电费/水费
    let calMonthCost = curry(function(...rest) {
        let costList = Array.from(rest)
        return costList.reduce((prev, cur) => {
            return prev + cur
        })
    })
    calMonthCost(1)
    calMonthCost(2)
    calMonthCost(3)
    calMonthCost(4)
    calMonthCost(5)
    // ...
    calMonthCost() // 结果 15
    2、可以提前传递部分参数
    常规写法
    function curry(mode) {
        return function(valstr) {
            return new RegExp(mode).test(valstr)
        }
    }
    let isMoblie = curry(/d{11}/)
    let isEmail = curry(/^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(.[a-zA-Z0-9-]+)*.[a-zA-Z0-9]{2,6}$/)
    console.log(isMoblie('13911111111'))    // true
    console.log(isEmail('test@qq.com'))     // true
    柯里化,扩展,分离
    function validate(mode, valstr) {
        return new RegExp(mode).test(valstr)
    }
    let isMoblie = curry(validate, /d{11}/)
    let isEmail = curry(validate, /^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(.[a-zA-Z0-9-]+)*.[a-zA-Z0-9]{2,6}$/)
    console.log(isMoblie('13911111111'))    // true
    console.log(isEmail('test@qq.com'))     // true
    3、bind实现(柯里化的一种)
    Function.prototype.bind = function(context) {
        let _this = this
        let args = [].slice.call(arguments, 1)
        return function() {
            return _this.apply(context, args.concat([].slice.call(arguments)))
        }
    }

    四、反柯里化

    反柯里化:扩大方法的适用范围
    1、可以让任何对象拥有其他对象的方法(改变原来方法上下文)
    2、增加被反柯里化方法接收的参数
    例子1
    function uncurrying(fn) {
        return function() {
            let args = [].slice.call(arguments)
            let that = args.shift()
            fn.apply(that, args)
        }
    }
    
    let person = {
        name: 'jolin',
        age: 18
    }
    let util = {
        sayPerson: function(...rest) {
            console.log('...rest :', ...rest)
            console.log('name: ', this.name, ', age: ', this.age)
        }
    }
    
    let uncurrySayPerson = uncurrying(util.sayPerson)
    uncurrySayPerson(person, 'test') // person 代表 util.sayPerson 的上下文,后面的都是参数 util.sayPerson 的参数
    // ...rest : test
    // name:  jolin , age:  18
    
    // 实际上平常我们是这么写的
    util.sayPerson.call(person, 'test') // person 代表 util.sayPerson 的上下文,后面的都是参数 util.sayPerson 的参数
    // ...rest : test
    // name:  jolin , age:  18

    例子2

    Function.prototype.uncurrying = function() {
        let _this = this // 这里指 Array.prototype.push
        return function() {
            return Function.prototype.call.apply(_this, arguments)
            // 1、这里暂时将 Function.prototype.call 中的 call 方法叫做 changeFn
            // 2、那么 Function.prototype.changeFn.apply(Array.prototype.push, arguments)
            // 3、Array.prototype.push.changeFn(arguments)
            // 4、changeFn 等于 Function.prototype.call 中的 call 方法
            // 5、最终等价于 Array.prototype.push.call(arguments)
            // 6、call 方法接受的第一个参数代表上下文,进一步拆分 Array.prototype.push.call(arguments[0], ...arguments[n-1])
        }
    }
    let push = Array.prototype.push.uncurrying()
    let obj = {}
    push(obj, 'hh') // obj 代表 Array.prototype.push 的上下文,后面的都是参数 Array.prototype.push 的参数
    
    // 实际上平常我们是这么写的
    Array.prototype.push.call(obj, 'hh') // obj 代表 Array.prototype.push 的上下文,后面的都是参数 Array.prototype.push 的参数
    都读到最后了、留下个建议如何
  • 相关阅读:
    文件打包下载
    DES加密解密
    jQuery实现表格拖动排序
    jQuery实现星星评分功能
    问卷调查功能中的题目编辑功能
    使用JS或jQuery模拟鼠标点击a标签事件
    zTree的使用
    给文本框添加模糊搜索功能(“我记录”MVC框架下实现)
    表达式计算器的实现
    asp.net几种开源上传控件,flash,ajax版,支持多文件
  • 原文地址:https://www.cnblogs.com/linjunfu/p/10898607.html
Copyright © 2011-2022 走看看