控制JavaScript函数的柯里化

原文链接

Haskellscala都支撑函数的柯里化,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函数式编程的库,人人可以看一下:

迎接提意见:可以在这里提意见

参考的材料

    原文作者:dreamapplehappy
    原文地址: https://segmentfault.com/a/1190000006096034
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞