深入解析JavaScript中函数的Currying柯里化,深入剖

2019-09-01 18:56 来源:未知

引子 先来看一道小问题:
有人在群里出了到一道题目:
var s = sum(1)(2)(3) ....... 最后 alert(s) 出来是6 
var s = sum(1)(2)(3)(4) ....... 最后 alert(s) 出来是10 
问sum怎么实现?
刚看到题目,我第一反应是sum返回的是一个function,但是没有最终实现,印象中看到过类似的原理,但是记不清了。
 
后来同事说,这个是叫柯里化,
实现方法比较巧妙:  

深入解析JavaScript中函数的Currying柯里化,currying柯里化

引子 先来看一道小问题:
有人在群里出了到一道题目:
var s = sum(1)(2)(3) ....... 最后 alert(s) 出来是6 
var s = sum(1)(2)(3)(4) ....... 最后 alert(s) 出来是10 
问sum怎么实现?
刚看到题目,我第一反应是sum返回的是一个function,但是没有最终实现,印象中看到过类似的原理,但是记不清了。
 
后来同事说,这个是叫柯里化,
实现方法比较巧妙:  

function sum(x){ 
 var y = function(x){ 
  return sum(x y) 
 } 
 y.toString = y.valueOf = function(){ 
  return x; 
 } 
 return y; 
} 

下面我们就深入来看一下currying柯里化~

什么是柯里化?

柯里化是这样的一个转换过程,把接受多个参数的函数变换成接受一个单一参数(注:最初函数的第一个参数)的函数,如果其他的参数是必要的,返回接受余下的参数且返回结果的新函数。

当我们这么说的时候,我想柯里化听起来相当简单。JavaScript中是怎么实现的呢?
假设我们要写一个函数,接受3个参数。

var sendMsg = function (from, to, msg) {
 alert(["Hello "   to   ",", msg, "Sincerely,", "- "   from].join("n"));
};

现在,假定我们有柯里化函数,能够把传统的JavaScript函数转换成柯里化后的函数:

var sendMsgCurried = curry(sendMsg); 
// returns function(a,b,c)

var sendMsgFromJohnToBob = sendMsgCurried("John")("Bob"); 
// returns function(c)

sendMsgFromJohnToBob("Come join the curry party!"); 
//=> "Hello Bob, Come join the curry party! Sincerely, - John"

手动柯里化

在上面的例子中,我们假定拥有神秘的curry函数。我会实现这样的函数,但是现在,我们首先看看为什么这样的函数是如此必要。
举个例子,手动柯里化一个函数并不困难,但是确实有点啰嗦:

// uncurried
var example1 = function (a, b, c) {

// do something with a, b, and c
};

// curried
var example2 = function(a) {
 return function (b) {
  return function (c) {

// do something with a, b, and c
  };
 };
};

在JavaScript,即使你不指定一个函数所有的参数,函数仍将被调用。这是个非常实用JavaScript的功能,但是却给柯里化制造了麻烦。

思路是每一个函数都是有且只有一个参数的函数。如果你想拥有多个参数,你必须定义一系列相互嵌套的函数。讨厌!这样做一次两次还可以,可是需要以这种方式定义需要很多参数的函数的时候,就会变得相当啰嗦和难于阅读。(但是别担心,我会马上告诉你一个办法)

一些函数编程语言,像Haskell和OCaml,语法中内置了函数柯里化。在这些语言中,举个例子,每个函数是拥有一个参数的函数,并且只有一个参数。你可能会认为这种限制麻烦胜过好处,但是语言的语法就是这样,这种限制几乎无法察觉。

举个例子,在OCaml,你可以用两种方式定义上面example:

let example1 = fun a b c ->

// (* do something with a, b, c *)

let example2 = fun a ->
 fun b ->
  fun c ->

// (* do something with a, b, c *)

很容易看出这两个例子和上面的那两个例子是如何的相似。

区别,然而,是否在OCaml也是做了同样的事情。OCaml,没有拥有多个参数的函数。但是,在一行中声明多个参数就是嵌套定义单参函数“快捷方式”。

类似的 ,我们期待调用柯里化函数句法上和OCaml中调用多参函数类似。我们期望这样调用上面的函数:

example1 foo bar baz
example2 foo bar baz

而在JavaScript,我们采用明显不同的方式:

example1(foo, bar, baz);
example2(foo)(bar)(baz);

在OCaml这类语言中,柯里化是内置的。在JavaScript,柯里化虽然可行(高阶函数),但是语法上是不方便的。这也是为什么我们决定编写一个柯里化函数来帮我们做这些繁琐的事情,并使得我们的代码简洁。

创建一个curry辅助函数

理论上我们期望可以有一个方便的方式转换普通老式的JavaScript函数(多个参数)到完全柯里化的函数。

这个想法不是我独有的,其他的人已经实现过了,例如在wu.js 库中的.autoCurry()函数(尽管你关心的是我们自己的实现方式)。

首先,让我们创建一个简单的辅助函数 .sub_curry:

function sub_curry(fn 
/*, variable number of args */
) {
 var args = [].slice.call(arguments, 1);
 return function () {
  return fn.apply(this, args.concat(toArray(arguments)));
 };
}

让我们花点时间看看这个函数的功能。相当简单。sub_curry接受一个函数fn作为它的第一个参数,后面跟着任何数目的输入参数。返回的是一个函数,这个函数返回fn.apply执行结果,参数序列合并了该函数最初传入参数的,加上fn调用的时候传入参数的。

看例子:

var fn = function(a, b, c) { return [a, b, c]; };

// these are all equivalent
fn("a", "b", "c");
sub_curry(fn, "a")("b", "c");
sub_curry(fn, "a", "b")("c");
sub_curry(fn, "a", "b", "c")();
//=> ["a", "b", "c"]

很明显,这并不是我门想要的,但是看起来有点柯里化的意思了。现在我们将定义柯里化函数curry:

function curry(fn, length) {

// capture fn's # of parameters
 length = length || fn.length;
 return function () {
  if (arguments.length < length) {

// not all arguments have been specified. Curry once more.
   var combined = [fn].concat(toArray(arguments));
   return length - arguments.length > 0 
    ? curry(sub_curry.apply(this, combined), length - arguments.length)
    : sub_curry.call(this, combined );
  } else {

// all arguments have been specified, actually call function
   return fn.apply(this, arguments);
  }
 };
}

这个函数接受两个参数,一个函数和要“柯里化”的参数数目。第二个参数是可选的,如果省略,默认使用Function.prototype.length 属性,就是为了告诉你这个函数定义了几个参数。

最终,我们能够论证下面的行为:

var fn = curry(function(a, b, c) { return [a, b, c]; });

// these are all equivalent
fn("a", "b", "c");
fn("a", "b", "c");
fn("a", "b")("c");
fn("a")("b", "c");
fn("a")("b")("c");
//=> ["a", "b", "c"]

我知道你在想什么…

等等…什么?!

难道你疯了?应该是这样!我们现在能够在JavaScript中编写柯里化函数,表现就如同OCaml或者Haskell中的那些函数。甚至,如果我想要一次传递多个参数,我可以向我从前做的那样,用逗号分隔下参数就可以了。不需要参数间那些丑陋的括号,即使是它是柯里化后的。

这个相当有用,我会立即马上谈论这个,可是首先我要让这个Curry函数前进一小步。

柯里化和“洞”(“holes”)

尽管柯里化函数已经很牛了,但是它也让你必须花费点小心思在你所定义函数的参数顺序上。终究,柯里化的背后思路就是创建函数,更具体的功能,分离其他更多的通用功能,通过分步应用它们。

当然这个只能工作在当最左参数就是你想要分步应用的参数!

为了解决这个,在一些函数式编程语言中,会定义一个特殊的“占位变量”。通常会指定下划线来干这事,如过作为一个函数的参数被传入,就表明这个是可以“跳过的”。是尚待指定的。

这是非常有用的,当你想要分步应用(partially apply)一个特定函数,但是你想要分布应用(partially apply)的参数并不是最左参数。

举个例子,我们有这样的一个函数:

var sendAjax = function (url, data, options) { 
/* ... */
 }

也许我们想要定义一个新的函数,我们部分提供SendAjax函数特定的Options,但是允许url和data可以被指定。

当然了,我们能够相当简单的这样定义函数:

var sendPost = function (url, data) {
 return sendAjax(url, data, { type: "POST", contentType: "application/json" });
};

或者,使用使用约定的下划线方式,就像下面这样:

var sendPost = sendAjax( _ , _ , { type: "POST", contentType: "application/json" });

注意两个参数以下划线的方式传入。显然,JavaScript并不具备这样的原生支持,于是我们怎样才能这样做呢?

回过头让我们把curry函数变得智能一点…

首先我们把我们的“占位符”定义成一个全局变量。

var _ = {};

我们把它定义成对象字面量{},便于我们可以通过===操作符来判等。

不管你喜不喜欢,为了简单一点我们就使用_来做“占位符”。现在我们就可以定义新的curry函数,就像下面这样:

function curry (fn, length, args, holes) {
 length = length || fn.length;
 args = args || [];
 holes = holes || [];
 return function(){
  var _args = args.slice(0),
   _holes = holes.slice(0),
   argStart = _args.length,
   holeStart = _holes.length,
   arg, i;
  for(i = 0; i < arguments.length; i  ) {
   arg = arguments[i];
   if(arg === _ && holeStart) {
    holeStart--;
    _holes.push(_holes.shift()); 
// move hole from beginning to end
   } else if (arg === _) {
    _holes.push(argStart   i); 
// the position of the hole.
   } else if (holeStart) {
    holeStart--;
    _args.splice(_holes.shift(), 0, arg); 
// insert arg at index of hole
   } else {
    _args.push(arg);
   }
  }
  if(_args.length < length) {
   return curry.call(this, fn, length, _args, _holes);
  } else {
   return fn.apply(this, _args);
  }
 }
}

实际代码还是有着巨大不同的。 我们这里做了一些关于这些“洞”(holes)参数是什么的记录。概括而言,运行的职责是相同的。

展示下我们的新帮手,下面的语句都是等价的:

var f = curry(function(a, b, c) { return [a, b, c]; });
var g = curry(function(a, b, c, d, e) { return [a, b, c, d, e]; });

// all of these are equivalent
f("a","b","c");
f("a")("b")("c");
f("a", "b", "c");
f("a", _, "c")("b");
f( _, "b")("a", "c");
//=> ["a", "b", "c"]

// all of these are equivalent
g(1, 2, 3, 4, 5);
g(_, 2, 3, 4, 5)(1);
g(1, _, 3)(_, 4)(2)(5);
//=> [1, 2, 3, 4, 5]

疯狂吧?!

我为什么要关心?柯里化能够怎么帮助我?

你可能会停在这儿思考…

美高梅网投平台,这看起来挺酷而且…但是这真的能帮助我编写更好的代码?

这里有很多原因关于为什么函数柯里化是有用的。

函数柯里化允许和鼓励你分隔复杂功能变成更小更容易分析的部分。这些小的逻辑单元显然是更容易理解和测试的,然后你的应用就会变成干净而整洁的组合,由一些小单元组成的组合。

为了给一个简单的例子,让我们分别使用Vanilla.js, Underscore.js, and “函数化方式” (极端利用函数化特性)来编写CSV解析器。

Vanilla.js (Imperative)

//  String -> [String]
var processLine = function (line){
 var row, columns, j;
 columns = line.split(",");
 row = [];
 for(j = 0; j < columns.length; j  ) {
  row.push(columns[j].trim());
 }
};

//  String -> [[String]]
var parseCSV = function (csv){
 var table, lines, i; 
 lines = csv.split("n");
 table = [];
 for(i = 0; i < lines.length; i  ) {
  table.push(processLine(lines[i]));
 }
 return table;
};
Underscore.js

//  String -> [String]
var processLine = function (row) {
 return _.map(row.split(","), function (c) {
  return c.trim();
 });
};

//  String -> [[String]]
var parseCSV = function (csv){
 return _.map(csv.split("n"), processLine);
};

函数化方式

//  String -> [String]
var processLine = compose( map(trim) , split(",") );

//  String -> [[String]]
var parseCSV = compose( map(processLine) , split("n") );

所有这些例子功能上是等价的。我有意的尽可能的简单的编写这些。

想要达到某种效果是很难的,但是主观上这些例子,我真的认为最后一个例子,函数式方式的,体现了函数式编程背后的威力。

关于curry性能的备注

一些极度关注性能的人可以看看这里,我的意思是,关注下所有这些额外的事情?

通常,是这样,使用柯里化会有一些开销。取决于你正在做的是什么,可能会或不会,以明显的方式影响你。也就是说,我敢说几乎大多数情况,你的代码的拥有性能瓶颈首先来自其他原因,而不是这个。

有关性能,这里有一些事情必须牢记于心:

  • 存取arguments对象通常要比存取命名参数要慢一点
  • 一些老版本的浏览器在arguments.length的实现上是相当慢的
  • 使用fn.apply( … ) 和 fn.call( … )通常比直接调用fn( … ) 稍微慢点
  • 创建大量嵌套作用域和闭包函数会带来花销,无论是在内存还是速度上
  • 在大多是web应用中,“瓶颈”会发生在操控DOM上。这是非常不可能的,你在所有方面关注性能。显然,用不用上面的代码自行考虑。

curry化来源与数学家 Haskell Curry的名字 (编程语言 Haskell也是以他的名字命名)。
 
柯里化通常也称部分求值,其含义是给函数分步传递参数,每次传递参数后部分应用参数,并返回一个更具体的函数接受剩下的参数,这中间可嵌套多层这样的接受部分参数函数,直至返回最后结果。

浅析 JavaScript 中的 函数 currying 柯里化

curry化来源与数学家 Haskell Curry的名字 (编程语言 Haskell也是以他的名字命名)。

 

柯里化通常也称部分求值,其含义是给函数分步传递参数,每次传递参数后部分应用参数,并返回一个更具体的函数接受剩下的参数,这中间可嵌套多层这样的接受部分参数函数,直至返回最后结果。

因此柯里化的过程是逐步传参,逐步缩小函数的适用范围,逐步求解的过程。

 

柯里化一个求和函数

按照分步求值,我们看一个简单的例子

 

var concat3Words = function (a, b, c) {

    return a b c;

};

 

var concat3WordsCurrying = function(a) {

    return function (b) {

        return function (c) {

            return a b c;

        };

    };

};

console.log(concat3Words("foo ","bar ","baza"));            // foo bar baza

console.log(concat3WordsCurrying("foo "));                  // [Function]

console.log(concat3WordsCurrying("foo ")("bar ")("baza"));  // foo bar baza

可以看到, concat3WordsCurrying("foo ") 是一个 Function,每次调用都返回一个新的函数,该函数接受另一个调用,然后又返回一个新的函数,直至最后返回结果,分布求解,层层递进。(PS:这里利用了闭包的特点)

 

那么现在我们更进一步,如果要求可传递的参数不止3个,可以传任意多个参数,当不传参数时输出结果?

 

首先来个普通的实现:

 

var add = function(items){

    return items.reduce(function(a,b){

        return a b

    });

};

console.log(add([1,2,3,4]));

但如果要求把每个数乘以10之后再相加,那么:

 

var add = function (items,multi) {

    return items.map(function (item) {

        return item*multi;

    }).reduce(function (a, b) {

        return a b

    });

};

console.log(add([1, 2, 3, 4],10));

好在有 map 和 reduce 函数,假如按照这个模式,现在要把每项加1,再汇总,那么我们需要更换map中的函数。

 

下面看一下柯里化实现:

 

var adder = function () {

    var _args = [];

    return function () {

        if (arguments.length === 0) {

            return _args.reduce(function (a, b) {

                return a b;

            });

        }

        [].push.apply(_args, [].slice.call(arguments));

        return arguments.callee;

    }

};    

var sum = adder();

 

console.log(sum);     // Function

 

sum(100,200)(300);    // 调用形式灵活,一次调用可输入一个或者多个参数,并且支持链式调用

sum(400);

console.log(sum());   // 1000 (加总计算) 

上面 adder是柯里化了的函数,它返回一个新的函数,新的函数接收可分批次接受新的参数,延迟到最后一次计算。

 

通用的柯里化函数

更典型的柯里化会把最后一次的计算封装进一个函数中,再把这个函数作为参数传入柯里化函数,这样即清晰,又灵活。

例如 每项乘以10, 我们可以把处理函数作为参数传入:

 

var currying = function (fn) {

    var _args = [];

    return function () {

        if (arguments.length === 0) {

            return fn.apply(this, _args);

        }

        Array.prototype.push.apply(_args, [].slice.call(arguments));

        return arguments.callee;

    }

};

 

var multi=function () {

    var total = 0;

    for (var i = 0, c; c = arguments[i ];) {

        total = c;

    }

    return total;

};

 

var sum = currying(multi);  

  

sum(100,200)(300);

sum(400);

console.log(sum());     // 1000  (空白调用时才真正计算)

这样 sum = currying(multi),调用非常清晰,使用效果也堪称绚丽,例如要累加多个值,可以把多个值作为做个参数 sum(1,2,3),也可以支持链式的调用,sum(1)(2)(3)

 

柯里化的基础

上面的代码其实是一个高阶函数(high-order function), 高阶函数是指操作函数的函数,它接收一个或者多个函数作为参数,并返回一个新函数。此外,还依赖与闭包的特性,来保存中间过程中输入的参数。即:

 

函数可以作为参数传递

函数能够作为函数的返回值

闭包

柯里化的作用

延迟计算。上面的例子已经比较好低说明了。

参数复用。当在多次调用同一个函数,并且传递的参数绝大多数是相同的,那么该函数可能是一个很好的柯里化候选。

动态创建函数。这可以是在部分计算出结果后,在此基础上动态生成新的函数处理后面的业务,这样省略了重复计算。或者可以通过将要传入调用函数的参数子集,部分应用到函数中,从而动态创造出一个新函数,这个新函数保存了重复传入的参数(以后不必每次都传)。例如,事件浏览器添加事件的辅助方法:

 

 

 var addEvent = function(el, type, fn, capture) {

     if (window.addEventListener) {

         el.addEventListener(type, function(e) {

             fn.call(el, e);

         }, capture);

     } else if (window.attachEvent) {

         el.attachEvent("on" type, function(e) {

             fn.call(el, e);

         });

     } 

 };

每次添加事件处理都要执行一遍 if...else...,其实在一个浏览器中只要一次判定就可以了,把根据一次判定之后的结果动态生成新的函数,以后就不必重新计算。

 

var addEvent = (function(){

    if (window.addEventListener) {

        return function(el, sType, fn, capture) {

            el.addEventListener(sType, function(e) {

                fn.call(el, e);

            }, (capture));

        };

    } else if (window.attachEvent) {

        return function(el, sType, fn, capture) {

            el.attachEvent("on" sType, function(e) {

                fn.call(el, e);

            });

        };

    }

})();

这个例子,第一次 if...else... 判断之后,完成了部分计算,动态创建新的函数来处理后面传入的参数,这是一个典型的柯里化。

 

Function.prototype.bind 方法也是柯里化应用

与 call/apply 方法直接执行不同,bind 方法 将第一个参数设置为函数执行的上下文,其他参数依次传递给调用方法(函数的主体本身不执行,可以看成是延迟执行),并动态创建返回一个新的函数, 这符合柯里化特点。

 

var foo = {x: 888};

var bar = function () {

    console.log(this.x);

}.bind(foo);               // 绑定

bar();                     // 888

下面是一个 bind 函数的模拟,testBind 创建并返回新的函数,在新的函数中将真正要执行业务的函数绑定到实参传入的上下文,延迟执行了。

 

 

Function.prototype.testBind = function (scope) {

    var fn = this;                    //// this 指向的是调用 testBind 方法的一个函数, 

    return function () {

        return fn.apply(scope);

    }

};

var testBindBar = bar.testBind(foo);  // 绑定 foo,延迟执行

console.log(testBindBar);             // Function (可见,bind之后返回的是一个延迟执行的新函数)

testBindBar();                        // 888

这里要注意 prototype 中 this 的理解。

JavaScript 中的 函数 currying 柯里化 curry化来源与数学家 Haskell Curry的名字 (编程语言 Haskell也是以他的名字命名)。 柯里化通常也称部...

function sum(x){ 
 var y = function(x){ 
  return sum(x y) 
 } 
 y.toString = y.valueOf = function(){ 
  return x; 
 } 
 return y; 
} 

您可能感兴趣的文章:

  • javascript中有趣的反柯里化深入分析
  • javascript的currying函数介绍
  • javascript currying返回函数的函数

引子 先来看一道小问题: 有人在群里出了到一道题目: var s = sum(1)(2)(3) ....... 最后...

因此柯里化的过程是逐步传参,逐步缩小函数的适用范围,逐步求解的过程。 

下面我们就深入来看一下currying柯里化~

柯里化一个求和函数
按照分步求值,我们看一个简单的例子

什么是柯里化?

var concat3Words = function (a, b, c) { 
  return a b c; 
}; 

var concat3WordsCurrying = function(a) { 
  return function (b) { 
    return function (c) { 
      return a b c; 
    }; 
  }; 
}; 
console.log(concat3Words("foo ","bar ","baza"));      // foo bar baza 
console.log(concat3WordsCurrying("foo "));         // [Function] 
console.log(concat3WordsCurrying("foo ")("bar ")("baza")); // foo bar baza 

柯里化是这样的一个转换过程,把接受多个参数的函数变换成接受一个单一参数(注:最初函数的第一个参数)的函数,如果其他的参数是必要的,返回接受余下的参数且返回结果的新函数。

可以看到, concat3WordsCurrying("foo ") 是一个 Function,每次调用都返回一个新的函数,该函数接受另一个调用,然后又返回一个新的函数,直至最后返回结果,分布求解,层层递进。(PS:这里利用了闭包的特点)   

当我们这么说的时候,我想柯里化听起来相当简单。JavaScript中是怎么实现的呢?
假设我们要写一个函数,接受3个参数。

那么现在我们更进一步,如果要求可传递的参数不止3个,可以传任意多个参数,当不传参数时输出结果? 

var sendMsg = function (from, to, msg) {
 alert(["Hello "   to   ",", msg, "Sincerely,", "- "   from].join("n"));
};

首先来个普通的实现:

现在,假定我们有柯里化函数,能够把传统的JavaScript函数转换成柯里化后的函数:

var add = function(items){ 
  return items.reduce(function(a,b){ 
    return a b 
  }); 
}; 
console.log(add([1,2,3,4])); 
var sendMsgCurried = curry(sendMsg); 
// returns function(a,b,c)

var sendMsgFromJohnToBob = sendMsgCurried("John")("Bob"); 
// returns function(c)

sendMsgFromJohnToBob("Come join the curry party!"); 
//=> "Hello Bob, Come join the curry party! Sincerely, - John"

但如果要求把每个数乘以10之后再相加,那么:

手动柯里化

var add = function (items,multi) { 
  return items.map(function (item) { 
    return item*multi; 
  }).reduce(function (a, b) { 
    return a   b 
  }); 
}; 
console.log(add([1, 2, 3, 4],10)); 

在上面的例子中,我们假定拥有神秘的curry函数。我会实现这样的函数,但是现在,我们首先看看为什么这样的函数是如此必要。
举个例子,手动柯里化一个函数并不困难,但是确实有点啰嗦:

好在有 map 和 reduce 函数,假如按照这个模式,现在要把每项加1,再汇总,那么我们需要更换map中的函数。 

// uncurried
var example1 = function (a, b, c) {

// do something with a, b, and c
};

// curried
var example2 = function(a) {
 return function (b) {
  return function (c) {

// do something with a, b, and c
  };
 };
};

下面看一下柯里化实现:

在JavaScript,即使你不指定一个函数所有的参数,函数仍将被调用。这是个非常实用JavaScript的功能,但是却给柯里化制造了麻烦。

var adder = function () { 
  var _args = []; 
  return function () { 
    if (arguments.length === 0) { 
      return _args.reduce(function (a, b) { 
        return a   b; 
      }); 
    } 
    [].push.apply(_args, [].slice.call(arguments)); 
    return arguments.callee; 
  } 
};   
var sum = adder(); 

console.log(sum);   // Function 

sum(100,200)(300);  // 调用形式灵活,一次调用可输入一个或者多个参数,并且支持链式调用 
sum(400); 
console.log(sum());  // 1000 (加总计算) 

思路是每一个函数都是有且只有一个参数的函数。如果你想拥有多个参数,你必须定义一系列相互嵌套的函数。讨厌!这样做一次两次还可以,可是需要以这种方式定义需要很多参数的函数的时候,就会变得相当啰嗦和难于阅读。(但是别担心,我会马上告诉你一个办法)

上面 adder是柯里化了的函数,它返回一个新的函数,新的函数接收可分批次接受新的参数,延迟到最后一次计算。   

一些函数编程语言,像Haskell和OCaml,语法中内置了函数柯里化。在这些语言中,举个例子,每个函数是拥有一个参数的函数,并且只有一个参数。你可能会认为这种限制麻烦胜过好处,但是语言的语法就是这样,这种限制几乎无法察觉。

通用的柯里化函数

举个例子,在OCaml,你可以用两种方式定义上面example:

更典型的柯里化会把最后一次的计算封装进一个函数中,再把这个函数作为参数传入柯里化函数,这样即清晰,又灵活。

let example1 = fun a b c ->

// (* do something with a, b, c *)

let example2 = fun a ->
 fun b ->
  fun c ->

// (* do something with a, b, c *)

例如 每项乘以10, 我们可以把处理函数作为参数传入:

很容易看出这两个例子和上面的那两个例子是如何的相似。

var currying = function (fn) { 
  var _args = []; 
  return function () { 
    if (arguments.length === 0) { 
      return fn.apply(this, _args); 
    } 
    Array.prototype.push.apply(_args, [].slice.call(arguments)); 
    return arguments.callee; 
  } 
}; 

var multi=function () { 
  var total = 0; 
  for (var i = 0, c; c = arguments[i  ];) { 
    total  = c; 
  } 
  return total; 
}; 

var sum = currying(multi);  

sum(100,200)(300); 
sum(400); 
console.log(sum());   // 1000 (空白调用时才真正计算)

区别,然而,是否在OCaml也是做了同样的事情。OCaml,没有拥有多个参数的函数。但是,在一行中声明多个参数就是嵌套定义单参函数“快捷方式”。

这样 sum = currying(multi),调用非常清晰,使用效果也堪称绚丽,例如要累加多个值,可以把多个值作为做个参数 sum(1,2,3),也可以支持链式的调用,sum(1)(2)(3)
 
柯里化的基础

类似的 ,我们期待调用柯里化函数句法上和OCaml中调用多参函数类似。我们期望这样调用上面的函数:

上面的代码其实是一个高阶函数(high-order function), 高阶函数是指操作函数的函数,它接收一个或者多个函数作为参数,并返回一个新函数。此外,还依赖与闭包的特性,来保存中间过程中输入的参数。即:
 
函数可以作为参数传递
函数能够作为函数的返回值
闭包
柯里化的作用
延迟计算。上面的例子已经比较好低说明了。

example1 foo bar baz
example2 foo bar baz

参数复用。当在多次调用同一个函数,并且传递的参数绝大多数是相同的,那么该函数可能是一个很好的柯里化候选。

而在JavaScript,我们采用明显不同的方式:

动态创建函数。这可以是在部分计算出结果后,在此基础上动态生成新的函数处理后面的业务,这样省略了重复计算。或者可以通过将要传入调用函数的参数子集,部分应用到函数中,从而动态创造出一个新函数,这个新函数保存了重复传入的参数(以后不必每次都传)。例如,事件浏览器添加事件的辅助方法:

example1(foo, bar, baz);
example2(foo)(bar)(baz);
 var addEvent = function(el, type, fn, capture) { 
   if (window.addEventListener) { 
     el.addEventListener(type, function(e) { 
       fn.call(el, e); 
     }, capture); 
   } else if (window.attachEvent) { 
     el.attachEvent("on"   type, function(e) { 
       fn.call(el, e); 
     }); 
   } 
 }; 

在OCaml这类语言中,柯里化是内置的。在JavaScript,柯里化虽然可行(高阶函数),但是语法上是不方便的。这也是为什么我们决定编写一个柯里化函数来帮我们做这些繁琐的事情,并使得我们的代码简洁。

每次添加事件处理都要执行一遍 if...else...,其实在一个浏览器中只要一次判定就可以了,把根据一次判定之后的结果动态生成新的函数,以后就不必重新计算。

创建一个curry辅助函数

var addEvent = (function(){ 
  if (window.addEventListener) { 
    return function(el, sType, fn, capture) { 
      el.addEventListener(sType, function(e) { 
        fn.call(el, e); 
      }, (capture)); 
    }; 
  } else if (window.attachEvent) { 
    return function(el, sType, fn, capture) { 
      el.attachEvent("on"   sType, function(e) { 
        fn.call(el, e); 
      }); 
    }; 
  } 
})(); 

理论上我们期望可以有一个方便的方式转换普通老式的JavaScript函数(多个参数)到完全柯里化的函数。

这个例子,第一次 if...else... 判断之后,完成了部分计算,动态创建新的函数来处理后面传入的参数,这是一个典型的柯里化。
 
Function.prototype.bind 方法也是柯里化应用

这个想法不是我独有的,其他的人已经实现过了,例如在wu.js 库中的.autoCurry()函数(尽管你关心的是我们自己的实现方式)。

与 call/apply 方法直接执行不同,bind 方法 将第一个参数设置为函数执行的上下文,其他参数依次传递给调用方法(函数的主体本身不执行,可以看成是延迟执行),并动态创建返回一个新的函数, 这符合柯里化特点。

首先,让我们创建一个简单的辅助函数 .sub_curry:

var foo = {x: 888}; 
var bar = function () { 
  console.log(this.x); 
}.bind(foo);        // 绑定 
bar();           // 888 
function sub_curry(fn 
/*, variable number of args */
) {
 var args = [].slice.call(arguments, 1);
 return function () {
  return fn.apply(this, args.concat(toArray(arguments)));
 };
}

下面是一个 bind 函数的模拟,testBind 创建并返回新的函数,在新的函数中将真正要执行业务的函数绑定到实参传入的上下文,延迟执行了。

让我们花点时间看看这个函数的功能。相当简单。sub_curry接受一个函数fn作为它的第一个参数,后面跟着任何数目的输入参数。返回的是一个函数,这个函数返回fn.apply执行结果,参数序列合并了该函数最初传入参数的,加上fn调用的时候传入参数的。

Function.prototype.testBind = function (scope) { 
  var fn = this;          //// this 指向的是调用 testBind 方法的一个函数, 
  return function () { 
    return fn.apply(scope); 
  } 
}; 
var testBindBar = bar.testBind(foo); // 绑定 foo,延迟执行 
console.log(testBindBar);       // Function (可见,bind之后返回的是一个延迟执行的新函数) 
testBindBar();            // 888 

看例子:

这里要注意 prototype 中 this 的理解。

var fn = function(a, b, c) { return [a, b, c]; };

// these are all equivalent
fn("a", "b", "c");
sub_curry(fn, "a")("b", "c");
sub_curry(fn, "a", "b")("c");
sub_curry(fn, "a", "b", "c")();
//=> ["a", "b", "c"]

以上这篇深入剖析JavaScript中的函数currying 柯里化就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持脚本之家。

很明显,这并不是我门想要的,但是看起来有点柯里化的意思了。现在我们将定义柯里化函数curry:

您可能感兴趣的文章:

  • javascript性能优化之分时函数的介绍
  • JavaScript中的高级函数
  • 深入解析JavaScript中函数的Currying柯里化
  • javascript中有趣的反柯里化深入分析
  • JavaScript函数柯里化详解
  • javascript中利用柯里化函数实现bind方法
  • javascript中利用柯里化函数实现bind方法【推荐】
  • 深入浅出理解JavaScript高级定时器原理与用法
  • Js setInterval与setTimeout(定时执行与循环执行)的代码(可以传入参数)
  • js定时器的使用(实例讲解)
  • JavaScript高级函数应用之分时函数实例分析
function curry(fn, length) {

// capture fn's # of parameters
 length = length || fn.length;
 return function () {
  if (arguments.length < length) {

// not all arguments have been specified. Curry once more.
   var combined = [fn].concat(toArray(arguments));
   return length - arguments.length > 0 
    ? curry(sub_curry.apply(this, combined), length - arguments.length)
    : sub_curry.call(this, combined );
  } else {

// all arguments have been specified, actually call function
   return fn.apply(this, arguments);
  }
 };
}

这个函数接受两个参数,一个函数和要“柯里化”的参数数目。第二个参数是可选的,如果省略,默认使用Function.prototype.length 属性,就是为了告诉你这个函数定义了几个参数。

最终,我们能够论证下面的行为:

var fn = curry(function(a, b, c) { return [a, b, c]; });

// these are all equivalent
fn("a", "b", "c");
fn("a", "b", "c");
fn("a", "b")("c");
fn("a")("b", "c");
fn("a")("b")("c");
//=> ["a", "b", "c"]

我知道你在想什么…

等等…什么?!

难道你疯了?应该是这样!我们现在能够在JavaScript中编写柯里化函数,表现就如同OCaml或者Haskell中的那些函数。甚至,如果我想要一次传递多个参数,我可以向我从前做的那样,用逗号分隔下参数就可以了。不需要参数间那些丑陋的括号,即使是它是柯里化后的。

这个相当有用,我会立即马上谈论这个,可是首先我要让这个Curry函数前进一小步。

柯里化和“洞”(“holes”)

尽管柯里化函数已经很牛了,但是它也让你必须花费点小心思在你所定义函数的参数顺序上。终究,柯里化的背后思路就是创建函数,更具体的功能,分离其他更多的通用功能,通过分步应用它们。

当然这个只能工作在当最左参数就是你想要分步应用的参数!

为了解决这个,在一些函数式编程语言中,会定义一个特殊的“占位变量”。通常会指定下划线来干这事,如过作为一个函数的参数被传入,就表明这个是可以“跳过的”。是尚待指定的。

这是非常有用的,当你想要分步应用(partially apply)一个特定函数,但是你想要分布应用(partially apply)的参数并不是最左参数。

举个例子,我们有这样的一个函数:

var sendAjax = function (url, data, options) { 
/* ... */
 }

也许我们想要定义一个新的函数,我们部分提供SendAjax函数特定的Options,但是允许url和data可以被指定。

当然了,我们能够相当简单的这样定义函数:

var sendPost = function (url, data) {
 return sendAjax(url, data, { type: "POST", contentType: "application/json" });
};

或者,使用使用约定的下划线方式,就像下面这样:

var sendPost = sendAjax( _ , _ , { type: "POST", contentType: "application/json" });

注意两个参数以下划线的方式传入。显然,JavaScript并不具备这样的原生支持,于是我们怎样才能这样做呢?

回过头让我们把curry函数变得智能一点…

首先我们把我们的“占位符”定义成一个全局变量。

var _ = {};

我们把它定义成对象字面量{},便于我们可以通过===操作符来判等。

不管你喜不喜欢,为了简单一点我们就使用_来做“占位符”。现在我们就可以定义新的curry函数,就像下面这样:

function curry (fn, length, args, holes) {
 length = length || fn.length;
 args = args || [];
 holes = holes || [];
 return function(){
  var _args = args.slice(0),
   _holes = holes.slice(0),
   argStart = _args.length,
   holeStart = _holes.length,
   arg, i;
  for(i = 0; i < arguments.length; i  ) {
   arg = arguments[i];
   if(arg === _ && holeStart) {
    holeStart--;
    _holes.push(_holes.shift()); 
// move hole from beginning to end
   } else if (arg === _) {
    _holes.push(argStart   i); 
// the position of the hole.
   } else if (holeStart) {
    holeStart--;
    _args.splice(_holes.shift(), 0, arg); 
// insert arg at index of hole
   } else {
    _args.push(arg);
   }
  }
  if(_args.length < length) {
   return curry.call(this, fn, length, _args, _holes);
  } else {
   return fn.apply(this, _args);
  }
 }
}

实际代码还是有着巨大不同的。 我们这里做了一些关于这些“洞”(holes)参数是什么的记录。概括而言,运行的职责是相同的。

展示下我们的新帮手,下面的语句都是等价的:

var f = curry(function(a, b, c) { return [a, b, c]; });
var g = curry(function(a, b, c, d, e) { return [a, b, c, d, e]; });

// all of these are equivalent
f("a","b","c");
f("a")("b")("c");
f("a", "b", "c");
f("a", _, "c")("b");
f( _, "b")("a", "c");
//=> ["a", "b", "c"]

// all of these are equivalent
g(1, 2, 3, 4, 5);
g(_, 2, 3, 4, 5)(1);
g(1, _, 3)(_, 4)(2)(5);
//=> [1, 2, 3, 4, 5]

疯狂吧?!

我为什么要关心?柯里化能够怎么帮助我?

你可能会停在这儿思考…

这看起来挺酷而且…但是这真的能帮助我编写更好的代码?

这里有很多原因关于为什么函数柯里化是有用的。

函数柯里化允许和鼓励你分隔复杂功能变成更小更容易分析的部分。这些小的逻辑单元显然是更容易理解和测试的,然后你的应用就会变成干净而整洁的组合,由一些小单元组成的组合。

为了给一个简单的例子,让我们分别使用Vanilla.js, Underscore.js, and “函数化方式” (极端利用函数化特性)来编写CSV解析器。

Vanilla.js (Imperative)

//  String -> [String]
var processLine = function (line){
 var row, columns, j;
 columns = line.split(",");
 row = [];
 for(j = 0; j < columns.length; j  ) {
  row.push(columns[j].trim());
 }
};

//  String -> [[String]]
var parseCSV = function (csv){
 var table, lines, i; 
 lines = csv.split("n");
 table = [];
 for(i = 0; i < lines.length; i  ) {
  table.push(processLine(lines[i]));
 }
 return table;
};
Underscore.js

//  String -> [String]
var processLine = function (row) {
 return _.map(row.split(","), function (c) {
  return c.trim();
 });
};

//  String -> [[String]]
var parseCSV = function (csv){
 return _.map(csv.split("n"), processLine);
};

函数化方式

//  String -> [String]
var processLine = compose( map(trim) , split(",") );

//  String -> [[String]]
var parseCSV = compose( map(processLine) , split("n") );

所有这些例子功能上是等价的。我有意的尽可能的简单的编写这些。

想要达到某种效果是很难的,但是主观上这些例子,我真的认为最后一个例子,函数式方式的,体现了函数式编程背后的威力。

关于curry性能的备注

一些极度关注性能的人可以看看这里,我的意思是,关注下所有这些额外的事情?

通常,是这样,使用柯里化会有一些开销。取决于你正在做的是什么,可能会或不会,以明显的方式影响你。也就是说,我敢说几乎大多数情况,你的代码的拥有性能瓶颈首先来自其他原因,而不是这个。

有关性能,这里有一些事情必须牢记于心:

  • 存取arguments对象通常要比存取命名参数要慢一点
  • 一些老版本的浏览器在arguments.length的实现上是相当慢的
  • 使用fn.apply( … ) 和 fn.call( … )通常比直接调用fn( … ) 稍微慢点
  • 创建大量嵌套作用域和闭包函数会带来花销,无论是在内存还是速度上
  • 在大多是web应用中,“瓶颈”会发生在操控DOM上。这是非常不可能的,你在所有方面关注性能。显然,用不用上面的代码自行考虑。

您可能感兴趣的文章:

  • javascript性能优化之分时函数的介绍
  • JavaScript中的高级函数
  • javascript中有趣的反柯里化深入分析
  • JavaScript函数柯里化详解
  • javascript中利用柯里化函数实现bind方法
  • 深入剖析JavaScript中的函数currying柯里化
  • javascript中利用柯里化函数实现bind方法【推荐】
  • 深入浅出理解JavaScript高级定时器原理与用法
  • Js setInterval与setTimeout(定时执行与循环执行)的代码(可以传入参数)
  • js定时器的使用(实例讲解)
  • JavaScript高级函数应用之分时函数实例分析
TAG标签:
版权声明:本文由美高梅网投平台发布于美高梅简介,转载请注明出处:深入解析JavaScript中函数的Currying柯里化,深入剖