美文网首页
JS函数柯里化

JS函数柯里化

作者: Mica_马超 | 来源:发表于2021-06-08 09:18 被阅读0次

    什么是柯里化?

    维基百科上说道:柯里化,英语:Currying(果然是满满的英译中的既视感),是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

    函数柯里化的主要作用和特点就是参数复用、提前返回和延迟执行。

    从一道经典面试题开始

    // 实现一个add方法,使计算结果能够满足如下预期:
    add(1)(2)(3) = 6;
    add(1, 2, 3)(4) = 10;
    add(1)(2)(3)(4)(5) = 15;
    

    简单实现

    // 普通的add函数
    function add(x, y) {
        return x + y
    }
    
    // Currying后
    function curryingAdd(x) {
        return function (y) {
            return x + y
        }
    }
    
    add(1, 2)           // 3
    curryingAdd(1)(2)   // 3
    

    实际上就是把add函数的x,y两个参数变成了先用一个函数接收x然后返回一个函数去处理y参数。现在思路应该就比较清晰了,就是只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

    但是问题来了费这么大劲封装一层,到底有什么用处呢?

    Currying有哪些好处呢?

    1. 参数复用
    // 正常正则验证字符串 reg.test(txt)
    
    // 函数封装后
    function check(reg, txt) {
        return reg.test(txt)
    }
    
    check(/\d+/g, 'test')       //false
    check(/[a-z]+/g, 'test')    //true
    
    // Currying后
    function curryingCheck(reg) {
        return function(txt) {
            return reg.test(txt)
        }
    }
    
    var hasNumber = curryingCheck(/\d+/g)
    var hasLetter = curryingCheck(/[a-z]+/g)
    
    hasNumber('test1')      // true
    hasNumber('testtest')   // false
    hasLetter('21212')      // false
    

    上面的示例是一个正则的校验,正常来说直接调用check函数就可以了,但是如果我有很多地方都要校验是否有数字,其实就是需要将第一个参数reg进行复用,这样别的地方就能够直接调用hasNumber,hasLetter等函数,让参数能够复用,调用起来也更方便。

    2. 提前确认
    var on = function(element, event, handler) {
        if (document.addEventListener) {
            if (element && event && handler) {
                element.addEventListener(event, handler, false);
            }
        } else {
            if (element && event && handler) {
                element.attachEvent('on' + event, handler);
            }
        }
    }
    
    var on = (function() {
        if (document.addEventListener) {
            return function(element, event, handler) {
                if (element && event && handler) {
                    element.addEventListener(event, handler, false);
                }
            };
        } else {
            return function(element, event, handler) {
                if (element && event && handler) {
                    element.attachEvent('on' + event, handler);
                }
            };
        }
    })();
    
    //换一种写法可能比较好理解一点,上面就是把isSupport这个参数给先确定下来了
    var on = function(isSupport, element, event, handler) {
        isSupport = isSupport || document.addEventListener;
        if (isSupport) {
            return element.addEventListener(event, handler, false);
        } else {
            return element.attachEvent('on' + event, handler);
        }
    }
    

    我们在做项目的过程中,封装一些dom操作可以说再常见不过,上面第一种写法也是比较常见,但是我们看看第二种写法,它相对一第一种写法就是自执行然后返回一个新的函数,这样其实就是提前确定了会走哪一个方法,避免每次都进行判断。

    3. 延迟运行
    Function.prototype.bind = function (context) {
        var _this = this
        var args = Array.prototype.slice.call(arguments, 1)
     
        return function() {
            return _this.apply(context, args)
        }
    }
    

    像我们js中经常使用的bind,实现的机制就是Currying.

    有没有什么通用的封装方法?

    // 初步封装
    var currying = function(fn) {
        // args 获取第一个方法内的全部参数
        var args = Array.prototype.slice.call(arguments, 1)
        return function() {
            // 将后面方法里的全部参数和args进行合并
            var newArgs = args.concat(Array.prototype.slice.call(arguments))
            // 把合并后的参数通过apply作为fn的参数并执行
            return fn.apply(this, newArgs)
        }
    }
    

    这边首先是初步封装,通过闭包把初步参数给保存下来,然后通过获取剩下的arguments进行拼接,最后执行需要currying的函数。

    但是好像还有些什么缺陷,这样返回的话其实只能多扩展一个参数,currying(a)(b)(c)这样的话,貌似就不支持了(不支持多参数调用),一般这种情况都会想到使用递归再进行封装一层。

    // 支持多参数传递
    function currying(fn, args) {
    
        var _this = this
        var len = fn.length;
        var args = args || [];
    
        return function() {
            var _args = Array.prototype.slice.call(arguments);
            Array.prototype.push.apply(args, _args);
    
            // 如果参数个数小于最初的fn.length,则递归调用,继续收集参数
            if (_args.length < len) {
                return progressCurrying.call(_this, fn, _args);
            }
    
            // 参数收集完毕,则执行fn
            return fn.apply(this, _args);
        }
    }
    

    这边其实是在初步的基础上,加上了递归的调用,只要参数个数小于最初的fn.length,就会继续执行递归。

    好处说完了,通用方法也有了,让我们来关注下curry的性能

    curry的一些性能问题你只要知道下面四点就差不多了:

    • 存取arguments对象通常要比存取命名参数要慢一点
    • 一些老版本的浏览器在arguments.length的实现上是相当慢的
    • 使用fn.apply( … ) 和 fn.call( … )通常比直接调用fn( … ) 稍微慢点
    • 创建大量嵌套作用域和闭包函数会带来花销,无论是在内存还是速度上

    其实在大部分应用中,主要的性能瓶颈是在操作DOM节点上,这js的性能损耗基本是可以忽略不计的,所以curry是可以直接放心的使用。

    最后再回到面试题

    // 实现一个add方法,使计算结果能够满足如下预期:
    add(1)(2)(3) = 6;
    add(1, 2, 3)(4) = 10;
    add(1)(2)(3)(4)(5) = 15;
    
    function add() {
        // 第一次执行时,定义一个数组专门用来存储所有的参数
        var _args = Array.prototype.slice.call(arguments);
    
        // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
        var _adder = function() {
            _args.push(...arguments);
            return _adder;
        };
    
        // 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
        _adder.toString = function () {
            return _args.reduce(function (a, b) {
                return a + b;
            });
        }
        return _adder;
    }
    
    add(1)(2)(3)                // 6
    add(1, 2, 3)(4)             // 10
    add(1)(2)(3)(4)(5)          // 15
    add(2, 6)(1)                // 9
    
    
    var add = function() {
        var _this = this,
        _args = arguments
        return function() {
            if (!arguments.length) {
                var sum = 0;
                for (var i = 0,
                c; c = _args[i++];) sum += c
                return sum
            } else {
                Array.prototype.push.apply(_args, arguments)
                return arguments.callee
            }
        }
    }
    add(1)(2)(3)(4)(); // 10
    

    es6版本

    const curry = (fn, arr = []) => (...args) => (
     arg => arg.length === fn.length
     ? fn(...arg)
     : curry(fn, arg)
    )([...arr, ...args])
    
    const add = (...args) => {
      return args.reduce(function (a, b) {
        return a + b;
      });
    }
    

    https://www.toutiao.com/i6694418057760080387/?tt_from=weixin&utm_campaign=client_share&wxshare_count=1&timestamp=1558920012&app=news_article&utm_source=weixin&utm_medium=toutiao_android&req_id=20190527092011010025066145987878F&group_id=6694418057760080387

    https://www.jianshu.com/p/2975c25e4d71
    https://www.cnblogs.com/pigtail/p/3447660.html

    相关文章

      网友评论

          本文标题:JS函数柯里化

          本文链接:https://www.haomeiwen.com/subject/ickctctx.html