浅谈函数柯里化

  关于函数柯里化的定义,我摘抄一段来自百度百科的原话:在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。

  这段话听起来可能有一些抽象,但是如果用实际例子来解释可能会帮助我们更好地理解何为函数柯里化。看看下面这个问题,是一道前端面试中常考的题:

如何实现add(2)(3)(4) = 9

  当我第一次看到这个题目的时候我就在思考,add(2)后面为什么还能带(3)(4)呢?是不是因为add(2)返回的是一个函数,所以它后面可以继续接收参数,可是如果这样的话它要怎么返回具体数值呢?于是我想到了一个可以实现储存参数的关键 - 闭包!是经过自己无数次尝试以及尝试失败之后,最终向命运妥协,开始到网上查阅资料,于是发现了以下这段代码,读者可以先仔细阅读一下下面这段代码,后面我们再来慢慢解析究竟何为函数柯里化,为什么函数柯里化就能解决这个问题?

 1 let myAdd = (a, b, c) => a+b+c;
 2 function curry(fn, args){
 3     let len = fn.length;
 4     let _this = this;
 5     let _args = args || [];
 6     return function(){
 7         let args = Array.prototype.slice.apply(arguments);
 8         args = Array.prototype.concat.call(_args, args);
 9         // 当接收到的参数小于fn所需参数个数时,继续接收参数
10         if(args.length < len){
11             return curry.call(_this, fn, args);
12         }
13         return fn.apply(this, args);
14     }
15 }
16 let add = curry(myAdd);
17 console.log(add(2)(3)(4));  // 9
18 console.loh(add(2,3)(4));   // 9
19 console.log(add(2,3,4));    // 9

  在上面的代码中,myAdd是一个可以接收三个参数并且返回三数之和的函数。通过一顿操作之后,它可以不用一次性接收三个参数,而是慢慢接收,当发现接收到的参数达到3个之后再返回结果。这就涉及到函数柯里化的第一个特点 --- 参数复用(后面再进行详解)。通过例子我们可以看到,实现这个特点主要原因是利用了闭包,将接收到的参数存于_args中,由于闭包的原因这些参数在函数执行完之后并不会被释放掉。

  上面的curry方法,将每次调用fn时读入的参数用args来保存,并将本次读入的参数args与当前一共拥有的所有参数_args用concat方法连接起来,当参数个数符合fn的参数个数要求时,则调用fn。(笔者不才,没办法想到用什么通俗易懂的语言来描述这个过程,只能将自己的理解表述出来)

  但是这个例子并不具有普遍性,如果我需要传入的参数不为3个那怎么办,所以对上面的例子进行修改之后可以得到下面这段代码

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

  上面这种写法存在一个弊端,就是返回值的类型是函数而不是数值,但是笔者目前也没找到什么更合适的方法了。

  解析完例子,我们来看看究竟什么是函数柯里化。柯里化一共有三大作用,分别是:

  • 参数复用
  • 提前确认
  • 延迟运行

  一、参数复用

  所谓参数复用,就是利用闭包的原理,让我们前面传输过来的参数不要被释放掉。看一下下面这段代码就显而易见了:

 1 // 正常封装check函数进行字符串正则匹配
 2 function check(reg, txt) {
 3     return reg.test(txt)
 4 }
 5 
 6 check(/\d+/g, 'test')        //false
 7 check(/[a-z]+/g, 'test')     //true
 8 
 9 // 使用柯里化函数进行字符串正则匹配
10 function curryingCheck(reg) {
11     return function(txt) {
12         return reg.test(txt)
13     }
14 }
15 
16 var hasNumber = curryingCheck(/\d+/g)
17 var hasLetter = curryingCheck(/[a-z]+/g)
18 
19 hasNumber('test1')      // true
20 hasNumber('testtest')   // false
21 hasLetter('21212')      // false

  二、 提前确认

  这一特性经常是用来对浏览器的兼容性做出一些判断并初始化api,比如说我们目前用来监听事件大部分情况是使用addEventListener来实现的,但是一些较久的浏览器并不支持该方法,所以在使用之前,我们可以先做一次判断,之后便可以省略这个步骤了。

 1 var on = (function() {
 2     if (document.addEventListener) {
 3         return function(element, event, handler) {
 4             if (element && event && handler) {
 5                 element.addEventListener(event, handler, false);
 6             }
 7         };
 8     } else {
 9         return function(element, event, handler) {
10             if (element && event && handler) {
11                 element.attachEvent('on' + event, handler);
12             }
13         };
14     }
15 })();

  三、 延迟运行

  js中的bind这个方法,用到的就是柯里化的这个特征。

1 Function.prototype.bind = function (context) {
2     var _this = this
3     var args = Array.prototype.slice.call(arguments, 1)
4  
5     return function() {
6         return _this.apply(context, args)
7     }
8 }

 

posted @ 2019-09-21 20:04  卑微小陈的随笔  阅读(4062)  评论(4编辑  收藏  举报