百分百源码网-让建站变得如此简单! 登录 注册 签到领金币!

主页 | 如何升级VIP | TAG标签

当前位置: 主页>网站教程>JS教程> 带你理解并把握JavaScript函数的柯里化
分享文章到:

带你理解并把握JavaScript函数的柯里化

发布时间:09/01 来源:未知 浏览: 关键词:

Haskell和scala都支撑函数的柯里化,JavaScript函数的柯里化还与JavaScript的函数编程有很大的联络,假如你感乐趣的话,可以在这些方面多下功夫理解,信赖收成必然许多.

看本篇文章需要知道的一些知识点

  • 函数部分的call/apply/arguments
  • 闭包
  • 高阶函数
  • 不完全函数

文章后面有对这些知识的简便说明,大家可以看看.

什么是柯里化?

我们先来看看维基百科中是怎样定义的:在运算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个简单参数(最初函数的第一个参数)的函数,并且返回接受余下的参数并且返回结果的新函数的技术。

我们可以举个简便的例子,如下函数add是一样的一个函数,就是将传进来的参数ab相加;函数curryingAdd就是对函数add停止柯里化的函数;
这样一来,本来我们需要直接传进去两个参数来停止运算的函数,此刻需要离别传入参数ab,函数如下:

function add(a, b) {
    return a + b;
}

function curryingAdd(a) {
    return function(b) {
        return a + b;
    }
}

add(1, 2); // 3
curryingAdd(1)(2); // 3

看到这里你大概会想,这样做有什么用?为什么要这样做?这样做能够给我们的利用带来什么样的好处?先别焦急,我们接着往下看.

为什么要对函数停止柯里化?

  • 可以使用一些小技巧(见下文)
  • 提早绑定好函数里面的某些参数,到达参数复用的结果,提高了适用性.
  • 牢固易变因素
  • 延迟运算

总之,函数的柯里化能够让你从新组合你的利用,把你的复杂功效拆分成一个一个的小部分,每一个小的部分都是简便的,便于懂得的,并且是容易测试的;

怎样对函数停止柯里化?

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

  • 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确实实现了我们想要的阿谁功效.并且我们也可以像使用本来的阿谁函数一样使用柯里化后的函数.

  • IV 泡椒凤爪

    我们已经能够写出很好的柯里化辅助函数了,但是这还不算是最刺激的,假如我们在传递参数的时候可以不依照顺来那必然很酷;当然我们也可以写出这样的函数来,
    这个crazyCurryingHelper函数如下所示:

    var _ = {};
    function crazyCurryingHelper(fn, length, args, holes) {
        length = length || fn.length;
        args   = args   || [];
        holes  = holes  || [];
    
        return function() {
            var _args       = args.slice(),
                _holes      = holes.slice();
    
            // 储备接收到的args和holes的长度
            var argLength   = _args.length,
                holeLength  = _holes.length;
    
            var allArgumentsSpecified = false;
    
            // 轮回
            var arg     = null,
                i       = 0,
                aLength = arguments.length;
    
            for(; i < aLength; i++) {
                arg = arguments[i];
    
                if(arg === _ && holeLength) {
                    // 轮回holes的位置
                    holeLength--;
                    _holes.push(_holes.shift());
                } else if (arg === _) {
                    // 储备hole就是_的位置
                    _holes.push(argLength + i);
                } else if (holeLength) {
                    // 可否还有没有弥补的hole
                    // 在参数列表指定hole的地方插入当前参数
                    holeLength--;
                    _args.splice(_holes.shift(), 0, arg);
                } else {
                    // 不需要弥补hole,直接增加到参数列表里面
                    _args.push(arg);
                }
            }
    
            // 推断可否所有的参数都已知足
            allArgumentsSpecified = (_args.length >= length);
            if(allArgumentsSpecified) {
                return fn.apply(this, _args);
            }
    
            // 递归的停止柯里化
            return crazyCurryingHelper.call(this, fn, length, _args, _holes);
        };
    }

    一些说明,我们使用_来表示参数中的那些缺失的参数,假如你使用了lodash的话,会有冲突的;那么你可以使用别的符号替换.
    依照一向的尿性,我们还是要验证一下这个crazyCurryingHelper是不是实现了我们所说的哪些功效,代码如下:

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

    结果显示,我们这个函数也实现了我们所说的那些功效.

柯里化的一些利用场景

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

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

    • 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办法也是使用函数的柯里化来完成的,详情可以看这里Function.prototype.bind().

    • 写出这样一个函数multiply(1)(2)(3) == 6结果为true,multiply(1)(2)(3)(...)(n) == (1)*(2)*(3)*(...)*(n)结果为true

      这个问题不知道大家碰到过没有,不外通过函数的柯里化,也是有方法解决的,看下面的代码:

      function multiply(x) {
          var y = function(x) {
              return multiply(x * y);
          };
          y.toString = y.valueOf = function() {
              return x;
          };
          return y;
      }
      
      console.log(multiply(1)(2)(3) == 6); // true
      console.log(multiply(1)(2)(3)(4)(5) == 120); // true

      由于multiply(1)(2)(3)的直接结果并不是6,而是一个函数对象{ [Number: 6] valueOf: [Function], toString: [Function] },我们
      之后使用了==会将左边这个函数对象转换成为一个数字,所以就到达了我们想要的结果.还有关于为什么使用toStringvalueOf办法
      可以看看这里的说明Number.prototype.valueOf(),Function.prototype.toString().

    • 上面的阿谁函数不足纯洁,我们也可以实现一个更纯洁的函数,但是可以会不太相符问题的要求.
      我们可以这样做,先把函数的参数储备,然后再对这些参数做处置,一旦有了这个思绪,我们就不难写出些面的代码:

      function add() {
          var args = Array.prototype.slice.call(arguments);
          var _that = this;
          return function() {
              var newArgs = Array.prototype.slice.call(arguments);
              var total = args.concat(newArgs);
              if(!arguments.length) {
                  var result = 1;
                  for(var i = 0; i < total.length; i++) {
                      result *= total[i];
                  }
                  return result;
              }
              else {
                  return add.apply(_that, total);
              }
          }
      }
      add(1)(2)(3)(); // 6
      add(1, 2, 3)(); // 6
    • 当我们的需要兼容IE9此前版本的IE阅读器的话,我们大概需要写出一些兼容的方案 ,比方事件监听;一样状况下我们应当会这样写:

      var addEvent = function (el, type, fn, capture) {
              if (window.addEventListener) {
                  el.addEventListener(type, fn, capture);
              }
              else {
                  el.attachEvent('on' + type, fn);
              }
          };

      这也写也是可以的,但是机能上会差一点,由于假如是在低版本的IE阅读器上每一次都会运转if()语句,发生了不必要的机能开销.
      我们也可以这样写:

      var addEvent = (function () {
              if (window.addEventListener) {
                  return function (el, type, fn, capture) {
                      el.addEventListener(type, fn, capture);
                  }
              }
              else {
                  return function (el, type, fn) {
                      var IEtype = 'on' + type;
                      el.attachEvent(IEtype, fn);
                  }
              }
          })();

      这样就减少了不必要的开支,整个函数运转一次就可以了.

  • 延迟运算

    上面的那两个函数multiply()add()实际上就是延迟运算的例子.

  • 提早绑定好函数里面的某些参数,到达参数复用的结果,提高了适用性.

    我们的I 开胃菜部分的tomLikejerryLike其实就是属于这种的,绑定好函数里面的第一个参数,然后后面按照状况离别使用不一样的函数.

  • 牢固易变因素

    我们经常使用的函数的bind办法就是一个牢固易变因素的很好的例子.

关于柯里化的机能

当然,使用柯里化意味着有一些额外的开销;这些开销一样触及到这些方面,第一是关于函数参数的调取,操纵arguments对象平常会比操纵命名的参数要慢一点;
还有,在一些老的版本的阅读器中arguments.length的实现是很慢的;直接调取函数fn要比使用fn.apply()或者fn.call()要快一点;发生大量的嵌套
作用域还有闭包会带来一些机能还有速度的落低.但是,大多数的web利用的机能瓶颈时发生在操纵DOM上的,所以上面的那些开销比起DOM操纵的开销还是比力小的.

关于本章一些知识点的说明

  • 琐碎的知识点

    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;这个函数就是我们盼望的阿谁函数.

  • 不完全函数(partial function)

    什么是不完全函数呢?所谓的不完全函数和我们上面所说的柯里化根本差不多;所谓的不完全函数,就是给你想要运转的阿谁函数绑定一个牢固的参数值;
    然后后面的运转或者说传递参数都是在前面的根基上停止运转的.看下面的例子:

    // 一个将函数的arguments对象变成一个数组的办法
    function array(a, n) {
        return Array.prototype.slice.call(a, n || 0);
    }
    // 我们要运转的函数
    function showMsg(a, b, c){
        return a * (b - c);
    }
    function partialLeft(f) {
        var args = arguments;
        return function() {
            var a = array(args, 1);
            a = a.concat(array(arguments));
            console.log(a); // 打印实际传递到函数中的参数列表
            return f.apply(this, a);
        }
    }
    function partialRight(f) {
        var args = arguments;
        return function() {
            var a = array(arguments);
            a = a.concat(array(args, 1));
            console.log(a); // 打印实际传递到函数中的参数列表
            return f.apply(this, a);
        }
    }
    function partial(f) {
        var args = arguments;
        return function() {
            var a = array(args, 1);
            var i = 0; j = 0;
            for(; i < a.length; i++) {
                if(a[i] === undefined) {
                    a[i] = arguments[j++];
                }
            }
            a = a.concat(array(arguments, j));
            console.log(a); // 打印实际传递到函数中的参数列表
            return f.apply(this, a);
        }
    }
    partialLeft(showMsg, 1)(2, 3); // 实际参数列表: [1, 2, 3] 所以结果是 1 * (2 - 3) = -1
    partialRight(showMsg, 1)(2, 3); // 实际参数列表: [2, 3, 1] 所以结果是 2 * (3 - 1) = 4
    partial(showMsg, undefined, 1)(2, 3); // 实际参数列表: [2, 1, 3] 所以结果是 2 * (1 - 3) = -4

一些你大概会喜爱的JS库

JavaScript的柯里化与JavaScript的函数式编程密不成分,下面列举了一些关于JavaScript函数式编程的库,大家可以看一下:

  • underscore
  • lodash
  • ramda
  • bacon.js
  • fn.js
  • functional-js

引荐教程:《JS教程》

以上就是带你理解并把握JavaScript函数的柯里化的具体内容,更多请关注百分百源码网其它相关文章!

打赏

打赏

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

百分百源码网 建议打赏1~10元,土豪随意,感谢您的阅读!

共有151人阅读,期待你的评论!发表评论
昵称: 网址: 验证码: 点击我更换图片
最新评论

本文标签

广告赞助

能出一分力是一分吧!

订阅获得更多模板

本文标签

广告赞助

订阅获得更多模板