函数柯里化(黑人问号脸)???Currying(黑人问号脸)??? 妥妥的中式翻译既视感; 下面来一起看看究竟什么是函数柯里化:
维基百科的解释是: 把接收多个参数的函数变换成接收一个单一参数 (最初函数的第一个参数) 的函数, 并返回接受剩余的参数而且返回结果的新函数的技术. 其由数学家 Haskell Brooks Curry 提出, 并以 curry 命名.
概念往往都是干涩且难懂的, 让我们用人话来解释就是: 如果我们不确定这个函数有多少个参数, 我们可以先给它传入一个参数, 然后通过 JS 闭包 (如若不懂 JS 闭包, 请先学习闭包知识点再来学习本篇博文 https://www.cnblogs.com/dengyao-blogs/p/11475575.html ) 来进行返回一个函数, 内部函数接收除开第一个参数外的其余参数进行操作并输出, 这个就是函数的柯里化;
举个小例子:
场景(需求):
众所周知程序员每天加班的时间还是比较多的, 如果我们需要计算一个程序员每天的加班时间, 那么我们的第一反应应该是这样;
- var overtime=0;
- function time(x){
- return overtime+=x;
- }
- time(1); //1
- time(2); //3
- time(3); //6
上面的代码固然没有问题, 可是需要每天调用都算加一下当天的时间, 很麻烦, 并且每调用一次函数都要进行一定的操作, 如果数据量巨大, 有可能会有影响性能的风险, 那么有没有可以偷懒又能解决问题的办法呢? 有的!
- function time(x){
- return function(y){
- return x+y;
- }
- }
- var times=time(0);
- times(3);
但是上面代码依然存在问题, 在实际开发中很多时候我们的参数是不确定的, 上面代码虽然简单的实现了柯里化的基本操作, 但是对于参数不确定的情况是处理不了的; 所以存在着函数参数的局限性; 不过我们从上面的代码中基本可以知道函数柯里化是个啥意思了; 就是一个函数调用的时候只允许传入一个参数, 然后通过闭包返回内部函数去处理和接收剩余参数, 返回的函数通过闭包的方式记住了 time 的第一个参数;
我们再来把代码改造一下:
- // 首先定义一个变量接收函数
- var overtime = (function() {
- // 定义一个数组用来接收参数
- var args = [];
- // 这里运用闭包, 调用外部函数返回一个内部函数
- return function() {
- //arguments 是浏览器内置对象, 专门用来接收参数
- // 如果参数的长度为 0 即没有参数的时候
- if(arguments.length === 0) {
- // 定义变量用来累加
- var time = 0;
- // 循环累加, 用 i 和 args 的长度进行比较
- for (var i = 0, l = args.length; i < l; i++) {
- // 进行累加操作 等价于 time=time+args[i]
- time += args[i];
- }
- // 返回累加的结果
- return time;
- // 如果 arguments 对象参数长度不为零, 即有参数的时候
- }else {
- // 定义的空数组添加 arguments 参数作为数组项, 第一个参数古 args 作为改变 this 指向, 第二个参数 arguments 把剩余参数作为数组形式添加至空数组中
- [].push.apply(args, arguments);
- }
- }
- })();
- overtime(3.5); // 第一天
- overtime(4.5); // 第二天
- overtime(2.1); // 第三天
- //...
- console.log( overtime() ); // 10.1
代码经过我们的改造已经实现了功能, 但是这不是一个函数柯里化的完整实现, 那么我们要怎么完整实现呢? 下面我们来介绍一种通用的实现方式:
通用的实现方式:
- // 定义方法 currying, 先传入一个参数
- var currying=function(fn){
- // 定义空数组装 arguments 对象的剩余参数
- var args=[];
- // 利用闭包返回一个函数处理剩余参数
- return function (){
- // 如果 arguments 的参数长度为 0, 即没有剩余参数
- if(arguments.length===0){
- // 执行上面方法
- // 这里的 this 指向下面的 s, 类似于 s(), 代表参数长度为 0 的时候直接调用函数
- return fn.apply(this,args)
- }
- console.log(arguments)
- // 如果 arguments 的参数长度不为 0, 即还有剩余参数
- // 在数组的原型对象上添加数组, apply 用来更改 this 的指向为 args
- // 将 [].slice.call(arguments) 的数组添加到原型数组上
- // 这里的 [].slice.call(arguments)===Array.prototype.slice.call(arguments) 实质上就是将 arguments 对象转成数组并具有 slice 功能
- Array.prototype.push.apply(args,[].slice.call(arguments))
- //args.push([].slice.call(arguments))
- console.log(args)
- // 这里返回的 arguments.callee 是返回的闭包函数, callee 是 arguments 对象里面的一个属性, 用于返回正被执行的 function 对象
- return arguments.callee
- }
- }
- // 这里调用 currying 方法并传入 add 函数, 结果会返回闭包内部函数
- var s=currying(add);
- // 调用闭包内部函数, 当有参数的时候会将参数逐步添加到 args 数组中, 待没有参数传入的时候直接调用
- // 调用的时候支持链式操作
- s(1)(2)(3)();
- // 也可以一次性传入多个参数
- s(1,2,3);
- console.log(s());
JS 函数柯里化的优点:
1. 可以延迟计算, 即如果调用柯里化函数传入参数是不调用的, 会将参数添加到数组中存储, 等到没有参数传入的时候进行调用;
2. 参数复用, 当在多次调用同一个函数, 并且传递的参数绝大多数是相同的, 那么该函数可能是一个很好的柯里化候选.
世间万物相对, 有因必有果, 当然了, 有柯里化必然有反柯里化;
反柯里化(uncurrying), 从字面意思上来讲就是跟柯里化的意思相反; 其实真正的反柯里化的作用是扩大适用范围, 就是说当我们调用某个方法的时候, 不需要考虑这个对象自身在设计的过程中有没有这个方法, 只要这个方法适用于它, 我们就可以使用;(这里引用的是动态语言中的鸭子类型的思想)
在学习 JS 反柯里化之前, 我们先学习一下动态语言的鸭子类型思想, 以助于我们更好的理解:
动态语言鸭子类型思想(维基百科解释):
在程序设计中, 鸭子类型 (duck typing) 是动态类型的一种风格.
在这种风格中, 一个对象有效的语义, 不是由继承自特定的类或实现特定的接口, 而是由当前方法和属性的集合决定.
这个概念的名字来源于由 James Whitcomb Riley 提出的鸭子测试,"鸭子测试" 可以这样表述:
当看到一只鸟走起来像鸭子, 游泳起来像鸭子, 叫起来也像鸭子, 那么这只鸟就可以被称为鸭子.
理论上的解释往往干涩难懂, 换成人话来说就是: 你是你妈妈的儿子 / 女儿, 不管你是否优秀, 是否漂亮, 只要你是你妈亲生的, 那么你就是你妈的儿子 / 女儿; 换成鸭子类型就是, 只要你会呱呱叫, 走起来像鸭子, 只要你拥有的行为像鸭子, 不管你是不是鸭子, 那么你就可以被称为鸭子;
在 JavaScript 中有很多鸭子类型的引用, 比如我们在对一个变量进行赋值的时候, 显然是不需要考虑变量的类型的, 正是因为如此, JavaScript 才更加的灵活, 所以 JavaScript 是一门典型的动态类型语言;
我们来看一下反柯里化中是怎么引用鸭子类型的:
- // 函数原型对象上添加 uncurring 方法
- Function.prototype.uncurring = function() {
- // 改变 this 的指向
- // 这里的 this 指向是 Array.prototype.push
- var self = this;
- // 这里的闭包用来返回内部函数的执行
- return function() {
- // 创建一个变量, 在数组的原型对象上添加 shift 上面删除第一个参数
- // 改变数组 this 的指向为 arguments
- var obj = Array.prototype.shift.call(arguments);
- // 最后返回执行并给方法改变指向为 obj 也就是 arguments
- // 并传入 arguments 作为参数
- return self.apply(obj, arguments);
- };
- };
- // 数组原型对象上添加 uncurrying 方法
- var push = Array.prototype.push.uncurring();
- // 测试一下
- // 匿名函数自执行
- (function() {
- // 这里的 push 就是一个函数方法了
- // 相当于传入参数 arguments 和 4 两个参数, 但是在上面 shift 方法中删除第一个参数, 这里的 arguments 参数被截取了, 所以最后实际上只传入了 4
- push(arguments, 4);
- console.log(arguments); //[1, 2, 3, 4]
- // 匿名函数自调用并带入参数 1,2,3
- })(1, 2, 3)
到这里大家可以想一想 arguments 是一个接收参数的对象, 里面是没有 push 方法的, 那么 arguments 为什么能调用 push 方法呢?
这是因为代码 var push = Array.prototype.push.uncurring(); 在数组的原型对象的 push 方法上添加了 uncurring 方法, 然后在执行匿名函数的方法 push(arguments, 4); 时候实质上是在调用上面的方法在 Function 的原型对象上添加 uncurring 方法并返回一个闭包内部函数执行, 在执行的过程中因为 Array 原型对象上的 shift 方法会把 push(arguments, 4); 中的 arguments 截取, 所以其实方法的实际调用是 push(4), 所以最终的结果才是[1,2,3,4]
在《JavaScript 设计模式与开发实践》一书中, JS 函数的反柯里化的案例是这样写的:
- // 定义一个对象
- var obj = {
- "length":1,
- "0":1
- }
- // 在 Function 原型对象定义方法 uncurrying
- Function.prototype.uncurrying = function() {
- //this 指向 Array.prototype.push
- var self = this;
- // 闭包返回一个内部函数
- return function() {
- // 这里可以拆开理解
- // 首先执行 apply return
- //Function.prototype.call(Array.prototype.push[obj,2])
- // 然后 Array.prototype.push.call(obj,2)
- //call 改变指向 obj.push(2)
- // 所以最后结果就是 {0: 1, 1: 2, length: 2}
- return Function.prototype.call.apply(self, arguments);
- }
- }
- // 在
- var push = Array.prototype.push.uncurrying()
- push(obj, 2)
- console.log(obj);
- //{0: 1, 1: 2, length: 2}
上面的方式不好理解? 没关系, 咱们来个好理解的:
- Function.prototype.unCurrying = function () {
- var self = this;
- return function () {
- //[].slice.call(arguments,1)===Array.prototype.push.slice.call(arguments,1)===arguments.slice(1)
- return self.apply(arguments[0], [].slice.call(arguments, 1));
- };
- };
- var push = Array.prototype.push.uncurrying()
- console.log(push);
- push(obj,2) //{0: 1, 1: 2, length: 2}
- console.log(obj);
分析一下:
1, 首先在 Function 原型对象上添加 uncurrying 方法, 这样所有的 Function 都可以借用;
2, 返回一个闭包内部函数
3, 闭包函数返回的结果中返回的是调用方法, self 指向 Array.prototype.push,apply 方法中第一个参数是更改指向, 对应下面 push(obj,2)相当于更改指向为 obj.push(2)
4,apply 方法中第二个参数的 call 方法是更改指向为 arguments, 并且 arguments 中能使用 slice 方法, 等于 arguments.slice(1)
来源: http://www.jianshu.com/p/aee40d4b62e4