JavaScript 专题系列第十三篇,讲解函数柯里化以及如何实现一个 curry 函数git
维基百科中对柯里化 (Currying) 的定义为:github
In mathematics and computer science, currying is the technique of translating the evaluation of a function that takes multiple arguments (or a tuple of arguments) into evaluating a sequence of functions, each with a single argument.ajax
翻译成中文:app
在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。函数
举个例子:post
function add(a, b) { return a + b; } // 执行 add 函数,一次传入两个参数便可 add(1, 2) // 3 // 假设有一个 curry 函数能够作到柯里化 var addCurry = curry(add); addCurry(1)(2) // 3
咱们会讲到如何写出这个 curry 函数,而且会将这个 curry 函数写的很强大,可是在编写以前,咱们须要知道柯里化到底有什么用?this
举个例子:lua
// 示意而已 function ajax(type, url, data) { var xhr = new XMLHttpRequest(); xhr.open(type, url, true); xhr.send(data); } // 虽然 ajax 这个函数很是通用,但在重复调用的时候参数冗余 ajax('POST', 'www.test.com', "name=kevin") ajax('POST', 'www.test2.com', "name=kevin") ajax('POST', 'www.test3.com', "name=kevin") // 利用 curry var ajaxCurry = curry(ajax); // 以 POST 类型请求数据 var post = ajaxCurry('POST'); post('www.test.com', "name=kevin"); // 以 POST 类型请求来自于 www.test.com 的数据 var postFromTest = post('www.test.com'); postFromTest("name=kevin");
想一想 jQuery 虽然有 $.ajax 这样通用的方法,可是也有 $.get 和 $.post 的语法糖。(固然 jQuery 底层是不是这样作的,我就没有研究了)。url
curry 的这种用途能够理解为:参数复用。本质上是下降通用性,提升适用性。prototype
但是即使如此,是否是依然感受没什么用呢?
若是咱们仅仅是把参数一个一个传进去,意义可能不大,可是若是咱们是把柯里化后的函数传给其余函数好比 map 呢?
举个例子:
好比咱们有这样一段数据:
var person = [{name: 'kevin'}, {name: 'daisy'}]
若是咱们要获取全部的 name 值,咱们能够这样作:
var name = person.map(function (item) { return item.name; })
不过若是咱们有 curry 函数:
var prop = curry(function (key, obj) { return obj[key] }); var name = person.map(prop('name'))
咱们为了获取 name 属性还要再编写一个 prop 函数,是否是又麻烦了些?
可是要注意,prop 函数编写一次后,之后能够屡次使用,实际上代码从本来的三行精简成了一行,并且你看代码是否是更加易懂了?
person.map(prop('name'))
就好像直白的告诉你:person 对象遍历(map)获取(prop) name 属性。
是否是感受有点意思了呢?
将来咱们会接触到更多有关柯里化的应用,不过那是将来的事情了,如今咱们该编写这个 curry 函数了。
一个常常会看到的 curry 函数的实现为:
// 初版 var curry = function (fn) { var args = [].slice.call(arguments, 1); return function() { var newArgs = args.concat([].slice.call(arguments)); return fn.apply(this, newArgs); }; };
咱们能够这样使用:
function add(a, b) { return a + b; } var addCurry = curry(add, 1, 2); addCurry() // 3 //或者 var addCurry = curry(add, 1); addCurry(2) // 3 //或者 var addCurry = curry(add); addCurry(1, 2) // 3
已经有柯里化的感受了,可是尚未达到要求,不过咱们能够把这个函数用做辅助函数,帮助咱们写真正的 curry 函数。
// 第二版 function sub_curry(fn) { var args = [].slice.call(arguments, 1); return function() { return fn.apply(this, args.concat([].slice.call(arguments))); }; } function curry(fn, length) { length = length || fn.length; var slice = Array.prototype.slice; return function() { if (arguments.length < length) { var combined = [fn].concat(slice.call(arguments)); return curry(sub_curry.apply(this, combined), length - arguments.length); } else { return fn.apply(this, arguments); } }; }
咱们验证下这个函数:
var fn = curry(function(a, b, c) { return [a, b, c]; }); fn("a", "b", "c") // ["a", "b", "c"] fn("a", "b")("c") // ["a", "b", "c"] fn("a")("b")("c") // ["a", "b", "c"] fn("a")("b", "c") // ["a", "b", "c"]
效果已经达到咱们的预期,然而这个 curry 函数的实现好难理解呐……
为了让你们更好的理解这个 curry 函数,我给你们写个极简版的代码:
function sub_curry(fn){ return function(){ return fn() } } function curry(fn, length){ length = length || 4; return function(){ if (length > 1) { return curry(sub_curry(fn), --length) } else { return fn() } } } var fn0 = function(){ console.log(1) } var fn1 = curry(fn0) fn1()()()() // 1
你们先从理解这个 curry 函数开始。
当执行 fn1() 时,函数返回:
curry(sub_curry(fn0)) // 至关于 curry(function(){ return fn0() })
当执行 fn1()() 时,函数返回:
curry(sub_curry(function(){ return fn0() })) // 至关于 curry(function(){ return (function(){ return fn0() })() }) // 至关于 curry(function(){ return fn0() })
当执行 fn1()()() 时,函数返回:
// 跟 fn1()() 的分析过程同样 curry(function(){ return fn0() })
当执行 fn1()()()() 时,由于此时 length > 2 为 false,因此执行 fn():
fn() // 至关于 (function(){ return fn0() })() // 至关于 fn0() // 执行 fn0 函数,打印 1
再回到真正的 curry 函数,咱们如下面的例子为例:
var fn0 = function(a, b, c, d) { return [a, b, c, d]; } var fn1 = curry(fn0); fn1("a", "b")("c")("d")
当执行 fn1("a", "b") 时:
fn1("a", "b") // 至关于 curry(fn0)("a", "b") // 至关于 curry(sub_curry(fn0, "a", "b")) // 至关于 // 注意 ... 只是一个示意,表示该函数执行时传入的参数会做为 fn0 后面的参数传入 curry(function(...){ return fn0("a", "b", ...) })
当执行 fn1("a", "b")("c") 时,函数返回:
curry(sub_curry(function(...){ return fn0("a", "b", ...) }), "c") // 至关于 curry(function(...){ return (function(...) {return fn0("a", "b", ...)})("c") }) // 至关于 curry(function(...){ return fn0("a", "b", "c", ...) })
当执行 fn1("a", "b")("c")("d") 时,此时 arguments.length < length 为 false ,执行 fn(arguments),至关于:
(function(...){ return fn0("a", "b", "c", ...) })("d") // 至关于 fn0("a", "b", "c", "d")
函数执行结束。
因此,其实整段代码又很好理解:
sub_curry 的做用就是用函数包裹原函数,而后给原函数传入以前的参数,当执行 fn0(...)(...) 的时候,执行包裹函数,返回原函数,而后再调用 sub_curry 再包裹原函数,而后将新的参数混合旧的参数再传入原函数,直到函数参数的数目达到要求为止。
若是要明白 curry 函数的运行原理,你们仍是要动手写一遍,尝试着分析执行步骤。
固然了,若是你以为仍是没法理解,你能够选择下面这种实现方式,能够实现一样的效果:
function curry(fn, args) { length = fn.length; args = args || []; return function() { var _args = args.slice(0), arg, i; for (i = 0; i < arguments.length; i++) { arg = arguments[i]; _args.push(arg); } if (_args.length < length) { return curry.call(this, fn, _args); } else { return fn.apply(this, _args); } } } var fn = curry(function(a, b, c) { console.log([a, b, c]); }); fn("a", "b", "c") // ["a", "b", "c"] fn("a", "b")("c") // ["a", "b", "c"] fn("a")("b")("c") // ["a", "b", "c"] fn("a")("b", "c") // ["a", "b", "c"]
或许你们以为这种方式更好理解,又能实现同样的效果,为何不直接就讲这种呢?
由于想给你们介绍各类实现的方法嘛,不能由于难以理解就不给你们介绍呐~
curry 函数写到这里其实已经很完善了,可是注意这个函数的传参顺序必须是从左到右,根据形参的顺序依次传入,若是我不想根据这个顺序传呢?
咱们能够建立一个占位符,好比这样:
var fn = curry(function(a, b, c) { console.log([a, b, c]); }); fn("a", _, "c")("b") // ["a", "b", "c"]
咱们直接看第三版的代码:
// 第三版 function curry(fn, args, holes) { length = fn.length; args = args || []; holes = holes || []; return function() { var _args = args.slice(0), _holes = holes.slice(0), argsLen = args.length, holesLen = holes.length, arg, i, index = 0; for (i = 0; i < arguments.length; i++) { arg = arguments[i]; // 处理相似 fn(1, _, _, 4)(_, 3) 这种状况,index 须要指向 holes 正确的下标 if (arg === _ && holesLen) { index++ if (index > holesLen) { _args.push(arg); _holes.push(argsLen - 1 + index - holesLen) } } // 处理相似 fn(1)(_) 这种状况 else if (arg === _) { _args.push(arg); _holes.push(argsLen + i); } // 处理相似 fn(_, 2)(1) 这种状况 else if (holesLen) { // fn(_, 2)(_, 3) if (index >= holesLen) { _args.push(arg); } // fn(_, 2)(1) 用参数 1 替换占位符 else { _args.splice(_holes[index], 1, arg); _holes.splice(index, 1) } } else { _args.push(arg); } } if (_holes.length || _args.length < length) { return curry.call(this, fn, _args, _holes); } else { return fn.apply(this, _args); } } } var _ = {}; var fn = curry(function(a, b, c, d, e) { console.log([a, b, c, d, e]); }); // 验证 输出所有都是 [1, 2, 3, 4, 5] fn(1, 2, 3, 4, 5); fn(_, 2, 3, 4, 5)(1); fn(1, _, 3, 4, 5)(2); fn(1, _, 3)(_, 4)(2)(5); fn(1, _, _, 4)(_, 3)(2)(5); fn(_, 2)(_, _, 4)(1)(3)(5)
至此,咱们已经实现了一个强大的 curry 函数,但是这个 curry 函数符合柯里化的定义吗?柯里化但是将一个多参数的函数转换成多个单参数的函数,可是如今咱们不只能够传入一个参数,还能够一次传入两个参数,甚至更多参数……这看起来更像一个柯里化 (curry) 和偏函数 (partial application) 的综合应用,但是什么又是偏函数呢?下篇文章会讲到。
JavaScript专题系列目录地址:https://github.com/mqyqingfeng/Blog。
JavaScript专题系列预计写二十篇左右,主要研究平常开发中一些功能点的实现,好比防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特色是研(chao)究(xi) underscore 和 jQuery 的实现方式。
若是有错误或者不严谨的地方,请务必给予指正,十分感谢。若是喜欢或者有所启发,欢迎 star,对做者也是一种鼓励。