函数柯里化

294 阅读5分钟

在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

基本实现

简单点来说,就是允许函数不一次性接收所有参数,而是接收部分参数后返回一个接收剩余参数的函数。

function sum(num1, num2) {
  return num1 + num2
}

function add(num2) {
  return sum(5, num2)
}
console.log(sum(2, 3)) // 5
console.log(add(3)) // 8

上面这个例子定义了两个函数sum()和add(),后者add本质上是前者sum第一个参数固定为5的版本。虽然add并不是柯里化函数,但是展示了其基本概念。

我们把函数改进一下:

function sum(num1) {
  return function(num2) {
    return num1 + num2
  }
}
console.log(sum(2)(3)) // 5

首先定义了一个add函数,它接受一个参数并返回一个新的函数。调用add之后,num1通过闭包的方式保存在内存,返回的是一个匿名函数。传入第二个参数后,匿名函数调用内存中的第一个参数(作用域),返回结果。

也可以用ES6的方法简写

const sum = a => b => a+b
console.log(sum(2)(3)) // 5

持续改进

这里以一道面试题来作为范例:

如何实现add(1)(2)(3) = 6

按照上面的例子,我们可以很轻松的写出:

add = a => b => c => a+b+c

console.log(add(1)(2)(3)) //6

虽然实现了函数的柯里化,但是这样一层一层的嵌套函数不够优雅,可拓展性也太差。那么能不能通过一个专门的函数来让目标函数柯里化呢?

const sum = (a, b, c) => a+b+c
console.log(sum(1,2,3)) // 6

//库里函数
function curry(fn){
//获取传入的除了第一项(被柯里化的函数)的参数
  let args = Array.prototype.slice.call(arguments, 1) //[1]
  return function() {
    let innerArgs = Array.prototype.slice.call(arguments) //[2,3]
    let newArgs = args.concat(innerArgs) //[1,2,3]
    //返回函数,将所有参数传入
    return fn.apply(null,newArgs)
  }
}
const add = curry(sum,1)
console.log(add(2,3)) // 6

虽然能分开传入参数,这样好像和题目不一样啊?不能一个个分开传参数,写这么多代码干嘛?

问题出在哪儿呢?以上面的为例,当我们传入第一个参数,能够获取这个参数[1],并返回一个函数,但是第二个第三个参数,却需要一起传入[2,3]。这里我们需要做一个判断,当参数没传完,继续柯里化返回的函数(递归),当所有参数传完了,返回结果。

const sum = (a, b, c) => a+b+c

function curry(fn,args){
  //获取传入函数的参数的个数
  let length = fn.length;
  //存储传入的参数,最开始为空数组
  let innerArgs = args || [];
  //为了直观,打印了值,依次为[] [1] [1,2] [1,2,3]
  console.log(innerArgs)
  return function() {
    let newArgs = innerArgs.concat(Array.prototype.slice.call(arguments))
    //为了直观,打印了值,依次为[1] [1,2]
    console.log(newArgs)
    //做判断,如果存储的参数少于函数的参数,那么再次处理函数
    if(newArgs.length < length) {
      return curry.call(this, fn, newArgs)
    }else {
      return fn.apply(null, newArgs)
    }
  }
}

const add = curry(sum)

console.log(add(1)(2)(3)) // 6
console.log(add(1,2)(3)) // 6
console.log(add(1)(2,3)) // 6

这样,我们不仅满足了题目的要求,还更进一步了!

简化

虽然实现了要求,但是代码过于繁琐,能不能更简单些呢?当然!

抽离出柯里化的逻辑,就是判断传入函数的参数个数,不够就固定参数返回函数,传完了就将所有参数填进去返回,那么我们可以这样写:

const sum = (x,y,z) => x+y+z

console.log(sum(1,2,3)) // 6

const curry = (fn, parmas) => {
  //初始化参数数组
  if(!Array.isArray(parmas)){
    parmas = []
  }
  //ES6获取所有参数
  return function(...parameter){
    //判断参数是否传完
    if(parmas.length + parameter.length < fn.length) {
      return curry(fn,parmas.concat(parameter))
    }
    //参数传完就返回结果
    return fn.apply(null,parmas.concat(parameter))
  }
}

const cSum = curry(sum)

console.log(cSum(1)(2)(3)) // 6
console.log(cSum(1,2)(3)) // 6

作用

使用柯里化大致有三个方面参数复用、延迟计算、提前返回。

在上面的例子中,如果a的值一直是2,只改变b的值,那么使用普通函数

sum(2, 10)
sum(2, 8)
sum(2, 3)

而在柯里化之后,就像上面的例子中写的

sum = a => b => a+b
let add2 = sum(2)
console.log(add2(10)) // 12

而在这个过程中,如果使用柯里化前的代码,或当即就把结果计算出来,而在柯里化之后,我们可以现传入一个2,然后在想得到真正结果的时候再传入另一个参数,体现了延迟计算。

同时柯里化函数还可以 提前返回,很常见的一个例子,兼容现代浏览器以及IE浏览器的事件添加方法。我们正常情况不使用柯里化可能会这样写:

const addEvent = function(element, type, fn, capture) {
    if (window.addEventListener) {
        element.addEventListener(type, function(e) {
            fn.call(element, e);
        }, capture);
    } else if (window.attachEvent) {
        element.attachEvent("on" + type, function(e) {
            fn.call(element, e);
        });
    } 
};

上面的方法有什么问题呢?我们每次使用addEvent为元素添加事件的时候都会走一遍if...else其实只要一次判定就可以了。

const addEvent = (function(){
    if (window.addEventListener) {
        return function(element, type, fn, capture) {
            element.addEventListener(type, function(e) {
                fn.call(element, e);
            }, (capture));
        };
    } else if (window.attachEvent) {
        return function(element, type, fn, capture) {
            element.attachEvent("on" + type, function(e) {
                fn.call(element, e);
            });
        };
    }
})();

总结

这是一个日常工作中很少用到的知识,去搜索柯里化大多为了应付一些面试题,但是了解柯里化,进一步了解函数式编程,对编写可维护性代码有一定的帮助,多学一点总是好的,继续加油。