JS函数柯里化

原文链接:https://www.jianshu.com/p/f02148c64bed?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

首先看看柯里化到底是什么?

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

看这个解释有一点抽象,我们就拿被做了无数次示例的add函数,来做一个简单的实现。

// 普通的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有哪些好处呢?

 参数复用

// 正常正则验证字符串 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等函数,让参数能够复用,调用起来也更方便。

如何对函数进行柯里化?

在这一部分里,我们由浅入深的一步步来告诉大家如何对一个多参数的函数进行柯里化.其中用到的知识有闭包,高阶函数,不完全函数等等.

  • I 开胃菜

    假如我们要实现一个功能,就是输出语句name喜欢song,其中namesong都是可变参数;那么一般情况下我们会这样写:

    function printInfo(name, song) {
        console.log(name + '喜欢的歌曲是: ' + song);
    }
    printInfo('Tom', '七里香');
    printInfo('Jerry', '雅俗共赏');
    

    对上面的函数进行柯里化之后,我们可以这样写:

    function curryingPrintInfo(name) {
        return function(song) {
            console.log(name + '喜欢的歌曲是: ' + song);
        }
    }
    var tomLike = curryingPrintInfo('Tom');
    tomLike('七里香');
    var jerryLike = curryingPrintInfo('Jerry');
    jerryLike('雅俗共赏');
    
  • II 小鸡炖蘑菇

    上面我们虽然对对函数printInfo进行了柯里化,但是我们可不想在需要柯里化的时候,都像上面那样不断地进行函数的嵌套,那简直是噩梦;
    所以我们要创造一些帮助其它函数进行柯里化的函数,我们暂且叫它为curryingHelper吧,一个简单的curryingHelper函数如下所示:

    function curryingHelper(fn) {
        var _args = Array.prototype.slice.call(arguments, 1);
        return function() {
            var _newArgs = Array.prototype.slice.call(arguments);
            var _totalArgs = _args.concat(_newArgs);
            return fn.apply(this, _totalArgs);
        }
    }
    

    这里解释一点东西,首先函数的arguments表示的是传递到函数中的参数对象,它不是一个数组,它是一个类数组对象;
    所以我们可以使用函数的Array.prototype.slice方法,然后使用.call方法来获取arguments里面的内容.
    我们使用fn.apply(this, _totalArgs)来给函数fn传递正确的参数.

    接下来我们来写一个简单的函数验证上面的辅助柯里化函数的正确性, 代码部分如下:

    function showMsg(name, age, fruit) {
        console.log('My name is ' + name + ', I\'m ' + age + ' years old, ' + ' and I like eat ' + fruit);
    }
    
    var curryingShowMsg1 = curryingHelper(showMsg, 'dreamapple');
    curryingShowMsg1(22, 'apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple
    
    var curryingShowMsg2 = curryingHelper(showMsg, 'dreamapple', 20);
    curryingShowMsg2('watermelon'); // My name is dreamapple, I'm 20 years old,  and I like eat watermelon
    

    上面的结果表示,我们的这个柯里化的函数是正确的.上面的curryingHelper就是一个高阶函数,关于高阶函数的解释可以参照下文.

  • III 牛肉火锅

    上面的柯里化帮助函数确实已经能够达到我们的一般性需求了,但是它还不够好,我们希望那些经过柯里化后的函数可以每次只传递进去一个参数,
    然后可以进行多次参数的传递,那么应该怎么办呢?我们可以再花费一些脑筋,写出一个betterCurryingHelper函数,实现我们上面说的那些
    功能.代码如下:

    function betterCurryingHelper(fn, len) {
        var length = len || fn.length;
        return function () {
            var allArgsFulfilled = (arguments.length >= length);
    
            // 如果参数全部满足,就可以终止递归调用
            if (allArgsFulfilled) {
                return fn.apply(this, arguments);
            }
            else {
                var argsNeedFulfilled = [fn].concat(Array.prototype.slice.call(arguments));
                return betterCurryingHelper(curryingHelper.apply(this, argsNeedFulfilled), length - arguments.length);
            }
        };
    }
    

    其中curryingHelper就是上面II 小鸡炖蘑菇中提及的那个函数.需要注意的是fn.length表示的是这个函数的参数长度.
    接下来我们来检验一下这个函数的正确性:

    var betterShowMsg = betterCurryingHelper(showMsg);
    betterShowMsg('dreamapple', 22, 'apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple
    betterShowMsg('dreamapple', 22)('apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple
    betterShowMsg('dreamapple')(22, 'apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple
    betterShowMsg('dreamapple')(22)('apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple
    

    其中showMsg就是II 小鸡炖蘑菇部分提及的那个函数.
    我们可以看出来,这个betterCurryingHelper确实实现了我们想要的那个功能.并且我们也可以像使用原来的那个函数一样使用柯里化后的函数.

柯里化的一些应用场景

说了那么多,其实这部分才是最重要的部分;学习某个知识要一定可以用得到,不然学习它干嘛.

  • 关于函数柯里化的一些小技巧

    • setTimeout传递地进来的函数添加参数

      一般情况下,我们如果想给一个setTimeout传递进来的函数添加参数的话,一般会使用之种方法:

      function hello(name) {
          console.log('Hello, ' + name);
      }
      setTimeout(hello('dreamapple'), 3600); //立即执行,不会在3.6s后执行
      setTimeout(function() {
          hello('dreamapple');
      }, 3600); // 3.6s 后执行
      

      我们使用了一个新的匿名函数包裹我们要执行的函数,然后在函数体里面给那个函数传递参数值.

      当然,在ES5里面,我们也可以使用函数的bind方法,如下所示:

      setTimeout(hello.bind(this, 'dreamapple'), 3600); // 3.6s 之后执行函数
      

      这样也是非常的方便快捷,并且可以绑定函数执行的上下文.

      我们本篇文章是讨论函数的柯里化,当然我们这里也可以使用函数的柯里化来达到这个效果:

      setTimeout(curryingHelper(hello, 'dreamapple'), 3600); // 其中curryingHelper是上面已经提及过的
      

      这样也是可以的,是不是很酷.其实函数的bind方法也是使用函数的柯里化来完成的,.

    • 最后再扩展一道经典面试题

      // 实现一个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
      


       

关于本章一些知识点的解释

  • 琐碎的知识点

    fn.length: 表示的是这个函数中参数的个数.

    arguments.callee: 指向的是当前运行的函数.calleearguments对象的属性。
    在该函数的函数体内,它可以指向当前正在执行的函数.当函数是匿名函数时,这是很有用的,比如没有名字的函数表达式(也被叫做"匿名函数").
    详细解释可以看这里arguments.callee.我们可以看一下下面的例子:

    function hello() {
        return function() {
            console.log('hello');
            if(!arguments.length) {
                console.log('from a anonymous function.');
                return arguments.callee;
            }
        }
    }
    
    hello()(1); // hello
    
    /*
     * hello
     * from a anonymous function.
     * hello
     * from a anonymous function.
     */
    hello()()();
    

    fn.caller: 返回调用指定函数的函数.详细的解释可以看这里Function.caller,下面是示例代码:

    function hello() {
        console.log('hello');
        console.log(hello.caller);
    }
    
    function callHello(fn) {
        return fn();
    }
    
    callHello(hello); // hello [Function: callHello]
    
  • 高阶函数(high-order function)

    高阶函数就是操作函数的函数,它接受一个或多个函数作为参数,并返回一个新的函数.
    我们来看一个例子,来帮助我们理解这个概念.就举一个我们高中经常遇到的场景,如下:

    f1(x, y) = x + y;
    f2(x) = x * x;
    f3 = f2(f3(x, y));
    

    我们来实现f3函数,看看应该如何实现,具体的代码如下所示:

    function f1(x, y) {
        return x + y;
    }
    
    function f2(x) {
        return x * x;
    }
    
    function func3(func1, func2) {
        return function() {
            return func2.call(this, func1.apply(this, arguments));
        }
    }
    
    var f3 = func3(f1, f2);
    console.log(f3(2, 3)); // 25
    

    我们通过函数func3将函数f1,f2结合到了一起,然后返回了一个新的函数f3;这个函数就是我们期望的那个函数.

一些你可能会喜欢的JS库

JavaScript的柯里化与JavaScript的函数式编程密不可分,

下面列举了一些关于JavaScript函数式编程的库,大家可以看一下:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值