JavaScript 专题系列第十三篇,讲解函数柯里化以及如何实现一个 curry 函数

定义

维基百科中对柯里化 (Currying) 的定义为:

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.

翻译成中文:

在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。

举个例子:

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

用途

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

举个例子:

// 示意而已
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 底层是否是这样做的,我就没有研究了)。

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

可是即便如此,是不是依然感觉没什么用呢?

如果我们仅仅是把参数一个一个传进去,意义可能不大,但是如果我们是把柯里化后的函数传给其他函数比如 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专题系列目录地址:github.com/mqyqingfeng…。

JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

JavaScript 专题之函数柯里化相关推荐

  1. 深入理解javascript系列(十七):函数柯里化

    之前的系列,我们介绍了什么是高阶函数.所有以函数作为参数的函数,都可以叫作高阶函数.并且我们常常利用高阶函数来封装一些公共逻辑. 本次,我们要继续学习,继续记录,柯里化.柯里化,其实就是高阶函数的一种 ...

  2. 深入详解python高级特性——函数柯里化(Currying)与反柯里化

    前言:本章的内容本来很简单,但是涉及到的理论部分相对较多,想要彻底弄懂前因后果需要具备以下几个知识点, (1)python的高阶函数 (2)python的装饰器本质 (3)Python的functoo ...

  3. JavaScript 中函数 柯里化风格的运用

    导语 当我第一次看见 柯里化 这个词语的时候,我也表现出一脸懵,在代码程序中,看见这种 "高大上"的一些词汇叫法的时候,下意识的会觉得这个概念很难很深奥,但是当冷静下来,去深究过后 ...

  4. 带你看懂javascript函数柯里化(currying)

    1.什么是柯里化 这里参照百度百科: 在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技 ...

  5. JavaScript函数柯里化详解

    目录 一.简单了解apply和call 二.什么是函数柯里化? 三.写一个公共的柯里化函数 四.创建一个灵活的柯里化函数 五.写一个可控制的执行时间的柯里化函数 一.简单了解apply和call ca ...

  6. 【JavaScript】函数柯里化

    文章目录 1. 什么是函数柯里化 2. 柯里化常用场景 2.1 参数复用 2.2 提前返回 2.3 延迟执行 3. 经典例题 3.1 封装通用柯里化函数 3.2 创建一个灵活的多步执行的柯里化函数 3 ...

  7. JavaScript函数柯里化

    一.简单了解apply和call call 和 apply 都是为了改变某个函数运行时的 context 即上下文而存在的,换句话说,就是为了改变函数体内部 this 的指向. call 和 appl ...

  8. 打造属于自己的underscore系列(五)- 偏函数和函数柯里化

    这一节的内容,主要针对javascript函数式编程的两个重要概念,偏函数(partial application) 和函数柯里化(curry)进行介绍.着重讲解underscore中对于偏函数应用的 ...

  9. reactjs高阶函数和函数柯里化

    高阶函数.函数柯里化 <!DOCTYPE html> <html lang="en"> <head><meta charset=" ...

最新文章

  1. Python快速学习10: 循环的对象及设计 (生活的规律)
  2. fiddler自动响应AutoResponder之正则匹配Rule Editor
  3. 前端学习(2815):小程序学习之开发者工具介绍
  4. ICCV2021 Oral | UNO:用于“新类发现”的统一目标函数,简化训练流程!已开源!...
  5. iFrame can't save session cookie
  6. Web API-排他思想及其案例
  7. Git:使用 GitHub 托管代码的简单流程
  8. ELK-部署Logstash
  9. html的跳转页面代码
  10. CDN (Content Delivery Network 内容分发网络)
  11. 分集与复用,分集用于抵抗信道衰落,复用用于提升系统容量
  12. Java数据结构与算法
  13. 面试笔记:面经-网易考拉
  14. Matlab实现 线性动态电路可视化分析
  15. javaweb课程设计:基于websocket的网络聊天室(所有的资源和代码还有详细步骤我都会提供)
  16. idea中各种标记文件夹的含义
  17. 天大2021年秋学期考试《西方经济学》离线作业考核试题
  18. 删除Mac版QQ聊天记录
  19. 【Javascript基础语法】第五周预习博客
  20. 计算机基本结构quiz

热门文章

  1. 在c语言中优先级最低的是6,C语言中 *,<<,= ,->哪个优先级最低
  2. Eclipse 中修改java编译版本
  3. openwrt使用linux内核版本,降低OpenWRT的Linux内核版本
  4. 写题过程中碰见的小问题
  5. leetcode129. 求根到叶子节点数字之和
  6. C++:21---仿函数
  7. 剑指offer(刷题31-40)--c++,Python版本
  8. 含有js的英文单词_JavaScript 常用单词整理
  9. ARM和NEON指令 very nice
  10. 如何成为一位杰出的程序员