JavaScript 中函数的柯里化

208 阅读4分钟

定义

柯里化是指将使用多个参数的函数转化成一系列使用一个参数的函数的技术,它返回一个新函数,这个新函数去处理剩下的参数。

举个例子

function add(a, b) {
    return a + b;
}

// 执行 add 函数,一次传入两个参数即可
add(1, 2) // 3

// 假设有一个 curry 函数可以做到柯里化
var addCurry = curry(add);
addCurry(1)(2) // 3

柯里化的作用

函数柯里化可以使你将复杂的功能分割成更小更容易分析的部分,这些小的逻辑单元显然是更容易理解和测试的,然后你的应用就会变成一系列干净而整洁的组合,由一些小单元组成的组合。

参数复用

本质上来说就是降低通用性,提高适用性。举个例子:

假设你有一个超市,你想给你的顾客 10% 的折扣:

function discount(price, discount) {
  return price * discount
}

当一个客户买了一件500元的商品,你会给他:

const price = discount(5000.1);

但是你会发现从长远来看,我们每天都自己计算10%的折扣。

const price = discount(8000.1);
const price = discount(7000.1);
const price = discount(10000.1);
const price = discount(15000.1);

这时,我们可以柯里化这个折扣函数

function discount(discount) {
 return (price) => {
   return price * discount;
 }
}  
const tenPercentDiscount = discount(0.1);
const twentyPercentDiscount = discount(0.2);

这时候我们只需要输入商品的价格就可以获得相应的折扣了,以后如果有其它的折扣也可以直接使用对应的折扣方法。

tenPercentDiscount(500); // 50
twentyPercentDiscount(500); // 100

2. 提前返回

看一个经典的例子: 元素绑定事件监听器:

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

以上代码是为了兼容 IE 浏览器对 DOM 事件绑定做的函数封装。

问题在于,每次对 DOM 元素进行事件绑定时,函数内部都会走一遍 if else。那么用函数柯里化就能实现提前返回

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

js 引擎在执行该段代码的时候就会进行兼容性判断,并且返回需要使用的事件监听封装函数。

3. 延迟计算

前面的例子中已经表明了,柯里化函数不会立即执行计算,第一次只是返回一个函数,后面的调用才会进行计算。

实现一个柯里化

实现一个curry函数,将普通函数进行柯里化:

function curry(fn, args = []) {
  return function(){
    let rest = [...args, ...arguments];
    if (rest.length < fn.length) {
       return curry.call(this, fn, rest);
    } else {
       return fn.apply(this, rest);
    }
  }
}

//test
function sum(a,b,c) {
    return a+b+c;
}
let sumFn = curry(sum);
console.log(sumFn(1)(2)(3)); //6
console.log(sumFn(1)(2, 3)); //6

我们有一个需求,用js 实现一个无限极累加的函数,实现如下预期

add(1) //=> 1;
add(1)(2)  //=> 2; 
add(1)(2)(3) //=>  6; 
add(1)(2)(3)(4) //=> 10; 

首先我们先讲一下函数的隐式转换。来看一个简单的题目:

function fn() {
    return 10;
}

console.log(fn + 10);

//function fn() {
//    return 10;
//}10

可以看出结果是函数体和10拼接成的字符串。

这时候稍微修改一下,再看看看:

function fn() {
    return 10;
}

fn.toString = function() {
    return 10;
}

console.log(fn + 10); // 20

还可以继续修改一下

function fn() {
    return 10;
}

fn.toString = function() {
    return 10;
}

fn.valueOf = function() {
    return 5;
}

console.log(fn + 10); // 15

首先我们要知道,当使用console.log,或者进行运算时,隐式转换就会发生。从上面三个相似的例子中我们可以得出一些结论:

当我们没有重新定义toString与valueOf时,函数的隐式转换会调用默认的toString方法,它会将函数的定义内容作为字符串返回。而当我们主动定义了toString/vauleOf方法时,那么隐式转换的返回结果则由我们自己控制了。其中valueOf的优先级会toString高一点。

现在我们再来进行一下包装

function add (a) {
  function sum (b) {
    a = a + b;
    return sum;
  }
  sum.toString = function() {
    return a;
  }
  return sum;
}
console.log(add(1)(2)(3)(4)); // 10

这时就实现了上面所说的需求,上面的代码通过闭包的形式来访问到变量a,第一次add(1)返回的其实是个函数,通过隐式转换调用了toString方法,返回的是保存的上次计算的结果。

下面我们把需求更进一步,要求还可以实现如下方式:

add(1, 2, 3)(4) = 10
function add () {
  const slice = Array.prototype.slice;
  const args = slice.call(arguments);

  function adder() {
    args.push(...slice.call(arguments));
    return adder;
  }
  adder.toString = () => {
    return args.reduce((a, b) => a + b, 0);
  }
  return adder;
}

上面的实现,利用闭包的特性,主要目的是想通过一些方法将所有的参数收集在一个数组里,并在最终隐式转换时将数组里的所有项加起来。