JS中的柯里化(currying)

这篇文章发布于 2013年02月26日,星期二,23:33,归类于 JS实例。 阅读 234748 次, 今日 7 次 66 条评论

 

一、柯里化和柯南的关系是?

回答:如果我说“柯里化 == 柯南”呢?
众人:博主,r u ok!? 是不是钓鱼钓久了脑袋秀逗了哈?柯里化可是函数式编程中的一个技巧,而柯南是到哪儿哪儿死人、10年不老的神话般的存在。八竿子都打不到的,怎会相等呢??

回答:诸位,眼睛睁大点,是==, 不是===哦~
众人:嗯哪,我眼睛已经瞪得灯泡大了,粑粑并没有变冰淇淋啊?

回答:这不就结了嘛。我说的是弱等于==, 又不是强等于===. 你想那,JS的世界里,0==false. 你看,这阿拉伯届的数字0,和英美届的单词false不就相等了,这两个家伙也是八竿子都达不到的哦。
众人这……好吧,村妇吵架道理多,恕我们愚钝,还是看不出来“柯里化==柯南”,喔~除了那个“柯”字是一样的,求解释~~

回答:百科中柯里化解释为:“@#¥#¥#%*&……%@#¥”,太术语了,我镜片看裂才知大概。若非要套用定义(见下面的①标注的引用),个人觉得:“柯里化”就像某些官员的把戏,官员要弄7个老婆,碍于国策(一夫一妻)以及年老弟衰,表面上就1个老婆,实际上剩下的6个暗地里消化。代码表示就是:

var currying = function(fn) {
    // fn 指官员消化老婆的手段
    var args = [].slice.call(arguments, 1);
    // args 指的是那个合法老婆
    return function() {
        // 已经有的老婆和新搞定的老婆们合成一体,方便控制
        var newArgs = args.concat([].slice.call(arguments));
        // 这些老婆们用 fn 这个手段消化利用,完成韦小宝前辈的壮举并返回
        return fn.apply(null, newArgs);
    };
};

// 下为官员如何搞定7个老婆的测试
// 获得合法老婆
var getWife = currying(function() {
    var allWife = [].slice.call(arguments);
    // allwife 就是所有的老婆的,包括暗渡陈仓进来的老婆
    console.log(allWife.join(";"));
}, "合法老婆");

// 获得其他6个老婆
getWife("大老婆","小老婆","俏老婆","刁蛮老婆","乖老婆","送上门老婆");

// 换一批老婆
getWife("超越韦小宝的老婆");

于是,结果就是:
官员-老婆-柯里化功能结果截图
众人:这与“柯南”童鞋有嘛关系?

① 柯里化(Currying),又称部分求值(Partial Evaluation),是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

回答:莫急莫急,且听臣妾一曲。百科上的定义是针对众多函数式语言而言的,按照Stoyan Stefanov(《JavaScript Pattern》作者)的说法,所谓“柯里化”就是使函数理解并处理部分应用,套用上面官员例子,就是只要有合法老婆和见不得的情妇就行,数目什么的随意。不过,如果这样理解,老婆的例子就不符合国情,反而更加契合的理解是“柯南”。
众人:哦?洗耳恭听!

回答:柯南身子虽小,但是里面住的却是大柯南,也就是一个function里面还有个function。不同柯南处理不同情况,例如,小柯南可以和…稍等,他女朋友叫什么的忘了,我查查…哦,毛利兰一起洗澡澡;但是大柯南就不行。小柯南不能当面指正犯人,需借助小五郎;但是,大柯南就可以直接质问指出凶手。就类似于,内外function处理不同的参数。如果代码表示就是(小柯南=smallKenan; 大柯南=bigKenan; 小柯南嗑药会变大柯南):

var smallKenan = function(action) {
    var bigKenan = function(doing) {
        var result = "";
        if (action === "take drugs") {
            if (doing === "bathWithGirlFriend") {
                result = "尖叫,新一,你这个色狼,然后一巴掌,脸煮熟了~";
            } else if (doing === "pointOutKiller") {
                result = "新一,这个案子就交给你的,快点找出谁是凶手吧~";
            }
        } else {
            if (doing === "bathWithGirlFriend") {
                result = "来吧,柯南,一起洗澡吧~";
            } else if (doing === "pointOutKiller") {
                result = "小孩子家,滚一边去!";
            }
        }
        console.log(result);
        return arguments.callee; // 等同于return bigKenan
    };
    return bigKenan;
};

// 小柯南吃药了,然后和毛利兰洗澡,凶案现场指证犯人;结果是……
smallKenan("take drugs")("bathWithGirlFriend")("pointOutKiller");

结果如下截图:
柯南吃药后洗澡指正凶犯结果

“吃药”、“洗澡”、“指出凶手”就可以看成三个参数,其中,“吃药”确实是小柯南使用的,而后面的是“洗澡”、“指出凶手”虽然跟在smallKenan()后面,实际上是大柯南使用的。这个就是柯里化,参数部分使用。外部函数处理部分应用,剩下的由外部函数的返回函数处理。

所以,我说“柯里化==柯南”不是胡扯吧~~
众人:我们擦~~,博主扯淡的功力TM现在更上一层楼啦,说得还真那么回事!

二、柯里化有什么作用?

众人:整这么多神乎其神的东西,有个毛线用?意淫一些所谓的技术,为了技术而技术最让人诟病了,我老老实实if else照样脚本跑得杠杠的!

回答:唉,大家所言极是。苍蝇蚊子嗡嗡不停也让人诟病,但是,也是有其存在的理由的,我们不妨看看~~
众人

回答:貌似柯里化有3个常见作用:1. 参数复用2. 提前返回;3. 延迟计算/运行
1. “参数复用”上面已经展示过了,官员老婆的例子就是,无论哪个官员,都是需要一个合法老婆;通过柯里化过程,getWife()无需添加这个多余的“合法老婆”参数。
2. “提前返回”,很常见的一个例子,兼容现代浏览器以及IE浏览器的事件添加方法。我们正常情况可能会这样写:

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);
        });
    } 
};

上面的方法有什么问题呢?很显然,我们每次使用addEvent为元素添加事件的时候,(eg. IE6/IE7)都会走一遍if...else if ...,其实只要一次判定就可以了,怎么做?–柯里化。改为下面这样子的代码:

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);
            });
        };
    }
})();

初始addEvent的执行其实值实现了部分的应用(只有一次的if...else if...判定),而剩余的参数应用都是其返回函数实现的,典型的柯里化。

3. “延迟计算”,一般而言,延迟计算或运行是没有必要的,因为一天花10块钱和月末花300块钱没什么本质区别——只是心里好受点(温水炖青蛙)。嘛,毕竟只是个人看法,您可能会不这么认为。举个例子,我每周末都要去钓鱼,我想知道我12月份4个周末总共钓了几斤鱼,把一些所谓的模式、概念抛开,我们可能就会下面这样实现:

var fishWeight = 0;
var addWeight = function(weight) {
    fishWeight += weight;
};

addWeight(2.3);
addWeight(6.5);
addWeight(1.2);
addWeight(2.5);

console.log(fishWeight);   // 12.5

每次addWeight都会累加鱼的总重量。

若是有柯里化实现,则会是下面这样:

var curryWeight = function(fn) {
    var _fishWeight = [];
    return function() {
        if (arguments.length === 0) {
            return fn.apply(null, _fishWeight);
        } else {
            _fishWeight = _fishWeight.concat([].slice.call(arguments));
        }
    }
};
var fishWeight = 0;
var addWeight = curryWeight(function() {
    var i=0; len = arguments.length;
    for (i; i<len; i+=1) {
        fishWeight += arguments[i];
    }
});

addWeight(2.3);
addWeight(6.5);
addWeight(1.2);
addWeight(2.5);
addWeight();    //  这里才计算

console.log(fishWeight);    // 12.5

众人:我勒个去,好高的楼啊,相比之下。

回答:确实,柯里化的实现似乎啰嗦了点。老妈的啰嗦显然不是用来消耗多余的口水的。这里的curryWeight方法啰嗦的意义在于柯里化的复用。比方说,我还想知道平均每次钓货的重量,则:

var averageWeight = 0;
var addWeight = curryWeight(function() {
    var i=0; len = arguments.length;
    for (i; i<len; i+=1) {
        averageWeight += arguments[i]/len;
    }
});

addWeight(2.3);
addWeight(6.5);
addWeight(1.2);
addWeight(2.5);
addWeight();    //  这里才计算

console.log(averageWeight);    // 3.125

虽然延迟计算听上去很高级,但是,恕我愚钝,我想破了脑袋也没想出哪种情况非要柯里化延迟计算实现才能显著提高性能。能想到的好处就是参数个数随意,比方说:

addWeight(2.3, 6.5);
addWeight(1.2, 2.5);

也是可以的。

补充于翌日:经人提点,发现自己忘了个东西,ES5中的bind方法,用来改变Function执行时候的上下文(函数主体本身不执行,与call/apply直接执行并改变不同),本质上就是延迟执行。例如:

var obj = {
    "name": "currying" 
},
fun = function() {
    console.log(this.name);
}.bind(obj);

fun(); // currying

从IE6~8的自定义扩展来看,其实现的机制就是柯里化(不考虑执行时的新增参数):

if (!function() {}.bind) {
    Function.prototype.bind = function(context) {
        var self = this
            , args = Array.prototype.slice.call(arguments);
            
        return function() {
            return self.apply(context, args.slice(1));    
        }
    };
}

关于ES5中bind方法自定义可参见我之前的“ECMAScript 5中bind方法、自定义及小拓展”一文。

三、没有问答的结束语

最近在看《JavaScript模式》一书,天哪,里面出现的各种设计模式(如工厂模式、外观模式、观察者模式),一双手都数不过来。而且这些名词又很抽象,书一合,马上大眼瞪小眼了。这种感觉就像是,房间里来了10个黑人,每个黑人都有一个“¥%#…¥”的名字,好不容易勉强记住了,灯一关房间一黑,等再开灯的时候,每个黑人的名字都变成…..”hello”了

其实这些模式在实际使用的时候,或多或少都使用过,当看到“**模式”概念的时候,我们就会猛然惊起:“哦,原来这个就叫做‘观察者模式’等”。现在要讨论的问题是,我们有没有必要把这些“**模式”都记住呢,都理解其对应的核心呢?这个问题类似于,我可以看懂NBA的篮球比赛,那我有没有必要把各个球队以及球队的队员都记住呢?

如果想成为JS大神,从这个目标来看,这是需要的;好比优秀的篮球解说员必须要知道每个球队的名字、球员甚至周边八卦。但是,现实很重要。如果连JS函数相关的基本东西都驾驭不好,显然,硬是啃这些似懂非懂的概念只会造成混乱。如果你觉得可以更近一步,先通透几个自己习惯的熟悉的使用模式,足够应付实际项目;其他一些概念什么的,更多的只是噱头,实用性其实并不大。正如本文的柯里化,看上去很高级,似乎也有点用处,然而JS的灵活性使得很多实现完全摆脱“柯里化”这个概念的束缚,以更通俗易懂的方式实现。

然而,即使实用性不高,我们还是要有所了解,因为,你不知道什么时候会用到它。比方说CSS中的display:table;某些情况下可以解决一些棘手问题(secret!).

因此,本文的柯里化至少要知道个大概,如果你囫囵吞枣式的看到此处,只记得“柯里化==柯南”,你可以再稍微静点心重新看一下。我的笔头要比嘴巴犀利的多,因为,文字我可以静心琢磨:“如何讲述才能提起兴趣,才能让新手也能知其意会其形?” ,久而久之,就形成了这种菊紧的风格——更适合新手阅读,同时被“高人”不屑:“写得太罗嗦啦,直接一句话不就完事啦!”还时不时来一句“鉴定完毕”,恩,挺适合去验尸

柯里化函数是有一个通用的方法的,在官员那个例子(currying()方法就是)其实已经展示了,不重复展示,至于代码的含义嘛,知者自知,不啰嗦。

好久没在结尾扯这么多乱七八糟的东西了,扯得心里像喝了常润茶般舒畅。本文内容,边学习变整理的,JS的概念我接触的也不多,文中错误难免,欢迎指正,欢迎交流,互相学习。

(本篇完)

分享到:


发表评论(目前66 条评论)

  1. 黑子君说道:

    看了评论笑出猪声,大神写的真好。

  2. 书生说道:

    单单理解这个没多大意义,不知道大家记得函数式编程的一个规定,那就是一个函数只能有一个参数,实现函数不耦合, 处理单一数据。

  3. yuuk说道:

    文中最后一段「边学习变整理的」变字应该是边字~ 原谅我只是指正了一个错别字!

  4. 欧阳和歌说道:

    关于addEvent的这个例子,感觉没有必要为了柯里化而柯里化吧,如果只需要做到仅判断一次,那么只需要用立即执行函数就已经可以完成任务了

    var addEvent = (function () {
    if (window.addEventListener) {
    return function(el, sType, fn, capture) {
    el.addEventListener(sType, test, capture)
    }
    } else if (window.attachEvent) {
    return function(el, sType, fn, capture) {
    el.attachEvent(“on” + sType, test)
    }
    }
    })()

    function test () {
    console.log(‘test’)
    }
    addEvent(window, ‘click’, test, false)
    addEvent(window, ‘click’, test, true)

    感觉这3个作用都太勉强啦(~ ̄▽ ̄)~

  5. 路由甲说道:

    旭哥威武

  6. 路人包子君说道:

    我觉得这个写的很好,风趣易懂,实乃大神,本包膜拜

  7. 路人甲说道:

    作为看这篇文章的打赏,我帮你点击了几个广告,不客气

  8. 红萝卜说道:

    我擦 你这博客写的好嗨啊

  9. tengmaoqing说道:

    鑫哥,你最后贴的bind函数实现,是错的啊

  10. Edwin说道:

    Function.prototype.bind = function(context) {
    var self = this;

    return function() {
    return self.apply(context, arguments);
    };
    };
    bind方法应该这样写吧,要不bind后生成的函数就不能传入函数本身的参数了

  11. Edwin说道:

    感觉currying就是返回函数的函数,在此函数闭包中定义了私有域变量。

  12. 杭盖说道:

    生平第一次留言:即不提函数式编程的场景,也不提 Curring 的惰性特征、高阶需求。几行代码可以说明清楚的问题,变成了哗众取宠。在布道方面,兔哥无疑是你的学习方向。

  13. find说道:

    哈哈,这篇博文需要笑着看

  14. XMwarrior说道:

    不错的讲解,浅显易懂,幽默风趣

  15. Capricair说道:

    display: table 能解决什么棘手问题,还请明示

  16. future说道:

    看了好几篇关于柯里化函数的博文,就感觉老哥的这篇最容易理解了,感觉这文采不是一般人能写的出来的啊! 赞 !!

  17. Glavo说道:

    柯里化和部分施用/部分求值是不同的概念

  18. chocolatl说道:

    吓的我去补了900集柯南

  19. hizhaoxiaoyang说道:

    我觉得楼主这样很生动有趣,柯南我虽然没看过。

    突然就想到柯里化再温故一下,于是搜索引擎排第一个就是这篇。

    13年的文章,感觉柯里化可以和几个新老概念触类旁通一下:偏函数、generator+yield、promise

  20. 路人说道:

    没看过柯南的路过,说的我一脸懵逼,只好自己跑一遍了,感谢博主提供实例。

  21. 轩辕Rowboat说道:

    怎么感觉事件绑定那段是惰性函数定义~

  22. 小明说道:

    谢谢老哥!

  23. Grand说道:

    大神就是大神啊!膜拜中……

  24. cz说道:

    谢谢楼主,楼主的幽默,很容易看进去

  25. 12说道:

    刚开始有点意思,挺容易理解的!可后来实在太多题外的知识碍于阻碍了,适当就好。好评,加油

  26. 就喜欢这个feel说道:

    卖萌加分。
    做人要有些乐趣;
    做程序员更甚;
    哼哼,
    好评

  27. 路过说道:

    无关紧要的话太多,干扰理解【我蠢】

  28. dennisleung说道:

    其实已经大概了解:
    currying就是利用函数式编程和闭包,达到拆分运算或者延迟运算。
    addEvent那里其实有运用到工厂模式。不知道撸主有没有注意到,它和设计模式中的xhr能力测试很类似。。。

  29. 不懂请不要bb说道:

    楼主装疯卖傻了半天其实根本没有解释清楚柯里化的目的

  30. 6娃说道:

    没看懂….自己太笨了。

  31. 不懂说道:

    博主太幽默了 每次看都是这样的: XD
    另 例子都很好 但对不熟悉的人来说确实有点难懂,要是能更简化一些就更好啦
    谢博主分享 学习很多!

  32. lioff说道:

    总是讲些无关的废话,干扰信息一大堆,别人点进来不是来看你写这些卖萌文,耍宝文,火星文,所以你也没必要这么自恋搞一堆废话在这里

  33. lvjs说道:

    博主讲的很清晰啊,例子很生动,赞赞赞~

  34. berwin说道:

    没必要把例子写的那么复杂。现在是懂的人不用看也会懂,不懂得人看了更不懂。。。

  35. 入职1.5年的菜鸟额说道:

    怎么办,看不懂哦

  36. memoryza说道:

    总算是看完了,非常感谢;看了一圈就对直观的 参数复用和提前返回有理解,但还是没脱离闭包;
    参数复用:无非就是有默认参数和一堆可选参数实现一个。
    那个延时计算没觉得非要是柯里化才有的作用

  37. 林帆说道:

    老师你好,看你的文章好长时间啦,一直把你当成我的榜样,嘻嘻,希望有一天也能成为向你一样的人。我是在学js的时候关注到你的博客的,受益匪浅。在学柯里化的时候,专门来你博客里面找资料,不过我比较愚钝。。js也是入门级的,还是不是很懂怎么样才算是柯里化,请问有没有其他浅显易懂的资料介绍,我想认真地学习这方面的东西。

  38. kueen说道:

    就是一堆的闭包!

  39. Keven说道:

    楼主写的方法有些抽象,可能是本人的JS水平太低了。

  40. zsq2010说道:

    叙述太复杂了…
    简单的说复杂,看起来就很累.

  41. sean.wang说道:

    挺喜欢博主的这种描述模式。

  42. 浩明1999说道:

    真心觉得《JavaScript模式》这本书不如远不如《JavaScript设计模式》

  43. t说道:

    弱弱问一句楼主用的是 IE 多少。。。

  44. Frank说道:

    文章不错。
    建议背景故事不要说的太复杂。

  45. 云水千寻说道:

    鑫哥,对于延迟计算函数curryWeight内的_fishWeight变量,它应该是一个局部变量吧?
    为什么执行ddWeight(2.3);
    addWeight(6.5);
    addWeight(1.2);
    addWeight(2.5);
    多次,每次addWeigh()执行后_fishWeight变量不被销毁,
    却能将多次执行的结果累加在一起呢?

    • 张 鑫旭说道:

      @云水千寻 莫非这就是大家常说的闭包!

    • asfsd说道:

      因为curryWeight这个对象没有被销毁,而且始终没有变,所以里面的属性_fishWeight始终都没变。addWeight实际上是curryWeight返回的一个函数,这个函数就是专门用来操作curryWeight里面的属性的。如果你对java比较熟悉,curryWeight相当于java里面的一个类的实例对象,_fishWeight相当于java里面类的成员变量,addWeight代表的这个函数就相当于java里面类的公用成员方法。

  46. derek说道:

    拜读,总结 无论 看多少视频,看多少书。 都得多多实践,练习, 和自我总结。
    今天 很开心。 希望以后张鑫旭 大神能多多指导。

  47. 飛弋说道:

    果然是高手,可以给我一份chm版的css3手册吗?网上下的都少了transform这的新功能

    我的那网站比起的你的这,太脆弱的,大神给个联系方式啊

  48. Michael J.说道:

    JavaScript设计模式已经看完了,单体模式就是jQuery所使用到的;另外,对于已经有传统类式继承经验的工程师来说,具有原型继承,闭包,函数式编程的JavaScript会让他们困惑许久。

  49. rambo说道:

    这种方法 采取的是一种闭包策略 假如经验不足的话 很容易会使内存溢出 尤其在处理大量dom元素的CRUD操作上! 并且你说的 延迟计算 我如果不看实例的话 还以为你说的是闭包的高级嵌套呢

  50. 葵中剑说道:

    我会吐槽告诉博主柯南是Conan而不是Cenan吗~~