原文地址:https://www.jianshu.com/p/2975c25e4d71

第一次看到柯里化这个词的时候,还是在看一篇算法相关的博客提到把函数柯里化,那时一看这个词就感觉很高端,实际上当你了解了后才发现其实就是高阶函数的一个特殊用法。
果然是不管作用怎么样都要有个高端的名字才有用。

首先看看柯里化到底是什么?

维基百科上说道:柯里化,英语:Currying(果然是满满的英译中的既视感),是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

看这个解释有一点抽象,我们就拿被做了无数次示例的add函数,来做一个简单的实现。

  1. // 普通的add函数
  2. function add(x, y) {
  3. return x + y
  4. }
  5. // Currying后
  6. function curryingAdd(x) {
  7. return function (y) {
  8. return x + y
  9. }
  10. }
  11. add(1, 2) // 3
  12. curryingAdd(1)(2) // 3

实际上就是把add函数的x,y两个参数变成了先用一个函数接收x然后返回一个函数去处理y参数。现在思路应该就比较清晰了,就是只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
但是问题来了费这么大劲封装一层,到底有什么用处呢?没有好处想让我们程序员多干事情是不可能滴,这辈子都不可能.

来列一列Currying有哪些好处呢?

1. 参数复用
  1. // 正常正则验证字符串 reg.test(txt)
  2. // 函数封装后
  3. function check(reg, txt) {
  4. return reg.test(txt)
  5. }
  6. check(/\d+/g, 'test') //false
  7. check(/[a-z]+/g, 'test') //true
  8. // Currying后
  9. function curryingCheck(reg) {
  10. return function(txt) {
  11. return reg.test(txt)
  12. }
  13. }
  14. var hasNumber = curryingCheck(/\d+/g)
  15. var hasLetter = curryingCheck(/[a-z]+/g)
  16. hasNumber('test1') // true
  17. hasNumber('testtest') // false
  18. hasLetter('21212') // false

上面的示例是一个正则的校验,正常来说直接调用check函数就可以了,但是如果我有很多地方都要校验是否有数字,其实就是需要将第一个参数reg进行复用,这样别的地方就能够直接调用hasNumber,hasLetter等函数,让参数能够复用,调用起来也更方便。

2. 提前确认
  1. var on = function(element, event, handler) {
  2. if (document.addEventListener) {
  3. if (element && event && handler) {
  4. element.addEventListener(event, handler, false);
  5. }
  6. } else {
  7. if (element && event && handler) {
  8. element.attachEvent('on' + event, handler);
  9. }
  10. }
  11. }
  12. var on = (function() {
  13. if (document.addEventListener) {
  14. return function(element, event, handler) {
  15. if (element && event && handler) {
  16. element.addEventListener(event, handler, false);
  17. }
  18. };
  19. } else {
  20. return function(element, event, handler) {
  21. if (element && event && handler) {
  22. element.attachEvent('on' + event, handler);
  23. }
  24. };
  25. }
  26. })();
  27. //换一种写法可能比较好理解一点,上面就是把isSupport这个参数给先确定下来了
  28. var on = function(isSupport, element, event, handler) {
  29. isSupport = isSupport || document.addEventListener;
  30. if (isSupport) {
  31. return element.addEventListener(event, handler, false);
  32. } else {
  33. return element.attachEvent('on' + event, handler);
  34. }
  35. }

我们在做项目的过程中,封装一些dom操作可以说再常见不过,上面第一种写法也是比较常见,但是我们看看第二种写法,它相对一第一种写法就是自执行然后返回一个新的函数,这样其实就是提前确定了会走哪一个方法,避免每次都进行判断。

3. 延迟运行
  1. Function.prototype.bind = function (context) {
  2. var _this = this
  3. var args = Array.prototype.slice.call(arguments, 1)
  4. return function() {
  5. return _this.apply(context, args)
  6. }
  7. }

像我们js中经常使用的bind,实现的机制就是Currying.
说了这几点好处之后,发现还有个问题,难道每次使用Currying都要对底层函数去做修改,

有没有什么通用的封装方法?

  1. // 初步封装
  2. var currying = function(fn) {
  3. // args 获取第一个方法内的全部参数
  4. var args = Array.prototype.slice.call(arguments, 1)
  5. return function() {
  6. // 将后面方法里的全部参数和args进行合并
  7. var newArgs = args.concat(Array.prototype.slice.call(arguments))
  8. // 把合并后的参数通过apply作为fn的参数并执行
  9. return fn.apply(this, newArgs)
  10. }
  11. }

这边首先是初步封装,通过闭包把初步参数给保存下来,然后通过获取剩下的arguments进行拼接,最后执行需要currying的函数。
但是好像还有些什么缺陷,这样返回的话其实只能多扩展一个参数,currying(a)(b)(c)这样的话,貌似就不支持了(不支持多参数调用),一般这种情况都会想到使用递归再进行封装一层。

  1. // 支持多参数传递
  2. function progressCurrying(fn, args) {
  3. var _this = this
  4. var len = fn.length;
  5. var args = args || [];
  6. return function() {
  7. var _args = Array.prototype.slice.call(arguments);
  8. Array.prototype.push.apply(args, _args);
  9. // 如果参数个数小于最初的fn.length,则递归调用,继续收集参数
  10. if (_args.length < len) {
  11. return progressCurrying.call(_this, fn, _args);
  12. }
  13. // 参数收集完毕,则执行fn
  14. return fn.apply(this, _args);
  15. }
  16. }

这边其实是在初步的基础上,加上了递归的调用,只要参数个数小于最初的fn.length,就会继续执行递归。

好处说完了,通用方法也有了,让我们来关注下curry的性能

curry的一些性能问题你只要知道下面四点就差不多了:

  • 存取arguments对象通常要比存取命名参数要慢一点
  • 一些老版本的浏览器在arguments.length的实现上是相当慢的
  • 使用fn.apply( … ) 和 fn.call( … )通常比直接调用fn( … ) 稍微慢点
  • 创建大量嵌套作用域和闭包函数会带来花销,无论是在内存还是速度上

其实在大部分应用中,主要的性能瓶颈是在操作DOM节点上,这js的性能损耗基本是可以忽略不计的,所以curry是可以直接放心的使用。

最后再扩展一道经典面试题

  1. // 实现一个add方法,使计算结果能够满足如下预期:
  2. add(1)(2)(3) = 6;
  3. add(1, 2, 3)(4) = 10;
  4. add(1)(2)(3)(4)(5) = 15;
  5. function add() {
  6. // 第一次执行时,定义一个数组专门用来存储所有的参数
  7. var _args = Array.prototype.slice.call(arguments);
  8. // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
  9. var _adder = function() {
  10. _args.push(...arguments);
  11. return _adder;
  12. };
  13. // 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
  14. _adder.toString = function () {
  15. return _args.reduce(function (a, b) {
  16. return a + b;
  17. });
  18. }
  19. return _adder;
  20. }
  21. add(1)(2)(3) // 6
  22. add(1, 2, 3)(4) // 10
  23. add(1)(2)(3)(4)(5) // 15
  24. add(2, 6)(1) // 9

参考的资料