简介
柯里化,即Currying的音译。Currying是编译原理实现多参函数的一个技术。

在说Javascript中柯里化之前,可以了解一下原始的Currying是什么,又是从何而来?

Currying 为实现多参函数提供一个递归讲解的实现思路—-把接受多个参数的函数变换成接受一个单一参数(最初的函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数

在某些编程语言中(如Haskell),是通过Currying技术支持多参函数这一语言特性的,所以Currying原本是一门编译原理层面的技术,用途是实现多参函数
**
举个例子:

  1. function add(a, b) {
  2. return a + b;
  3. }
  4. // 执行add函数,一次传入两个参数即可
  5. add(1, 2);
  6. // 假设有一个 curry函数可以做到柯里化
  7. var addCurry = curry(add);
  8. addCurry(1)(2) // 3

用途

我们会讲如何写出这个curry函数,并且会将这个curry函数写的很强大,但是在编写之前我们要知道curry函数到底有啥用?

举个例子:

  1. // 示意而已
  2. function ajax(type, url, data) {
  3. var xhr = new XMLHttpRequest();
  4. xhr.open(type, url, true);
  5. xhr.send(data);
  6. }
  7. // 虽然这个ajax函数非常通用,但是在重复调用的时候参数冗余
  8. ajax('GET', 'www.test.com', 'name=kevin');
  9. ajax('GET', 'www.test_1.com', 'name=john');
  10. ajax('GET', 'www.test_2.com', 'name=stephon');
  11. // 利用curry
  12. var ajaxCurry = curry(ajax);
  13. // 以 GET 类型请求数据
  14. var get = ajaxCurry('GET');
  15. get('www.test.com', 'name=kevin');
  16. // 以GET请求来自www.test,com的数据
  17. var getFromTest = curry(get);
  18. getFromTest('name=kevin');

在JQuery中虽然有$.ajax这种方法,但是也有$.get和$.post的语法糖。(JQuery底层是否是这样做的,我也没去研究了)

curry的这种用途可以理解为:参数复用。本质上是降低通用性,提高适用性。

但是这样看起来,怎么感觉更复杂了呢?

如果我们把参数一个个传进去,意义可能不大。但是如果我们是把柯里化后的函数传给其他函数,比如map呢?

举个例子:

比如我们有这样一段数据:

  1. var person = [{name: 'kevin'}, {name: 'john'}];

如果我们要获取所有的name值,我们可以这样做:

  1. var result = person.map(function(item) {
  2. return item.name;
  3. })

如果我们有curry函数的话,就是这样:

  1. var prop = curry(function(key, obj) {
  2. return obj[key];
  3. })
  4. var name = person.map(prop('name'));

我们为了获取name属性还要在编写一个prop函数,是不是又麻烦了些?

但是要注意的是,prop函数编写一次后,以后可以多次使用,实际上代码从三行缩减为一行,而且看代码是不是更加易懂了?

person.map(prop(‘name’))就好像直白的告诉你:person对象遍历(map)获取prop(name)的属性值。

或许看到这里可能还是一脸懵逼,不着急,看看下面的示例是否能理解?

第一版

未来我们会接触到更多有关柯里化的应用,不过那是未来的事了,现在我们先来编写这个curry函数。

一个经常会看到的curry函数的实现为:

  1. // 第一版
  2. var curry = function(fn) {
  3. var args = [].slice.call(arguments, 1); // 收集curry方法的非第一元素参数列表,并格式化类数组
  4. return function() {
  5. var newArgs = args.concat([].slice.call(arguments));
  6. return fn.apply(this, newArgs);
  7. }
  8. }

我们可以这样使用:

  1. function add(a, b) {
  2. return a + b;
  3. }
  4. var addCurry = curry(add, 1, 2);
  5. addCurry(); // 3
  6. // 或者
  7. var addCurry = curry(add, 1);
  8. addCurry(2); // 3
  9. // 又或者
  10. var addCurry = curry(add);
  11. addCurry(1, 2); // 3

已经有柯里化的感觉了, 但是还没有达到要求, 不过我们可以把这个函数用作辅助函数, 帮助我们写真正的curry函数

第二版

  1. // 第二版
  2. function sub_curry(fn) {
  3. var args = [].slice.call(arguments, 1);
  4. return function() {
  5. return fn.apply(this, args.concat([].slice.call(arguments)));
  6. }
  7. }
  8. function curry(fn, length) {
  9. var length = length || fn.length;
  10. var slice = Array.prototype.slice;
  11. return function() {
  12. if (arguments.length < length) {
  13. var combine = [fn].concat(slice.call(arguments));
  14. return curry(sub_curry.apply(this, combine), length - arguments.length);
  15. } else {
  16. return fn.apply(this, arguments);
  17. }
  18. };
  19. }

我们验证下这个函数:

  1. var fn = curry(function(a, b, c) {
  2. return [a, b, c];
  3. })
  4. fn(1, 2, 3); // [1, 2, 3]
  5. fn(1, 2)(3); // [1, 2, 3]
  6. fn(1)(2)(3); // [1, 2, 3]
  7. fn(1)(2, 3); // [1, 2, 3]

效果达到了我们的预期,但是这个curry实现有点难理解……(特别是这个combine的实现),还有一点小瑕疵,参数必须按照fn的规则来设置

为了让大家能更好的理解这个curry函数,我给大家写了一个极简版的代码

  1. function curry(fn, args) {
  2. var length = fn.length;
  3. args = args || [];
  4. return function() {
  5. var _args = args.slice(0), arg, i;
  6. for (i = 0; i < arguments.length; i++) {
  7. arg = arguments[i];
  8. _args.push(arg);
  9. }
  10. if (_args.length < length) {
  11. return curry.call(this, fn, _args);
  12. } else {
  13. return fn.apply(this, _args);
  14. }
  15. }
  16. }
  17. var fn = curry(function(a, b, c) {
  18. return [1, 2, 3];
  19. });
  20. fn(1, 2, 3); // [1, 2, 3]
  21. fn(1, 2)(3); // [1, 2, 3]
  22. fn(1)(2, 3); // [1, 2, 3]
  23. fn(1)(2)(3); // [1, 2, 3]

这个方法相对来说容易理解一些把~~

第三版

curry函数写到这里其实已经很完善了,但是注意这个函数的传餐顺序必须是从左至右,根据形象的顺序一次传入,如果我不想根据这个顺序传呢?

我们可以创建一个占位符,比如这样:

  1. var fn = curry(function(a, b, c) {
  2. console.log([a, b , c]);
  3. })
  4. fn(1, _, 2)(3); // [1, 2, 3]

我们直接来看第三版代码:

  1. // 第三版
  2. function curry(fn, args, holes) {
  3. length = fn.length;
  4. args = args || [];
  5. holes = holes || [];
  6. return function() {
  7. var _args = args.slice(0),
  8. _holes = holes.slice(0),
  9. argsLen = args.length,
  10. holesLen = holes.length,
  11. arg, i, index = 0;
  12. for (i = 0; i < arguments.length; i++) {
  13. arg = arguments[i];
  14. // 处理类似 fn(1, _, _, 4)(_, 3) 这种情况,index 需要指向 holes 正确的下标
  15. if (arg === _ && holesLen) {
  16. index++
  17. if (index > holesLen) {
  18. _args.push(arg);
  19. _holes.push(argsLen - 1 + index - holesLen)
  20. }
  21. }
  22. // 处理类似 fn(1)(_) 这种情况
  23. else if (arg === _) {
  24. _args.push(arg);
  25. _holes.push(argsLen + i);
  26. }
  27. // 处理类似 fn(_, 2)(1) 这种情况
  28. else if (holesLen) {
  29. // fn(_, 2)(_, 3)
  30. if (index >= holesLen) {
  31. _args.push(arg);
  32. }
  33. // fn(_, 2)(1) 用参数 1 替换占位符
  34. else {
  35. _args.splice(_holes[index], 1, arg);
  36. _holes.splice(index, 1)
  37. }
  38. }
  39. else {
  40. _args.push(arg);
  41. }
  42. }
  43. if (_holes.length || _args.length < length) {
  44. return curry.call(this, fn, _args, _holes);
  45. }
  46. else {
  47. return fn.apply(this, _args);
  48. }
  49. }
  50. }
  51. var _ = {};
  52. var fn = curry(function(a, b, c, d, e) {
  53. console.log([a, b, c, d, e]);
  54. });
  55. // 验证 输出全部都是 [1, 2, 3, 4, 5]
  56. fn(1, 2, 3, 4, 5);
  57. fn(_, 2, 3, 4, 5)(1);
  58. fn(1, _, 3, 4, 5)(2);
  59. fn(1, _, 3)(_, 4)(2)(5);
  60. fn(1, _, _, 4)(_, 3)(2)(5);
  61. fn(_, 2)(_, _, 4)(1)(3)(5)

至此,我们就实现了一个强大的curry函数,可是这个curry函数符合柯里化的定义吗?柯里化就是将多参数函数转化成多个单一参数的函数。

结论

起初我本来是不打算写这个文章的,在我google了一些函数柯里化的内容以后,我发现这个概念的实现方法比较复杂,我又不具备模拟的能力,但是既然看都看了,还是要写点东西,以备日后复习。
这篇文章主要是摘抄自冴羽大大的博客内容,具体我也只理解了前两版的内容。这篇博客跟我理解的函数柯里化还有些出入,可惜我基础太差,没法自己写一个比较完善的实现方法。这边还有另外以为大牛的博客也详细讲解了柯里化的模拟实现,大家可以去看下:深入详解函数的柯里化 。如果发现还不是很理解的话,可以自行google