一道面试题

已知一个add方法。

1
2
3
4
function add(a, b, c) {  
return a + b + c;
}
add(1,2,3) // 6

实现一个sum方法,满足如下条件:

1
2
sum(1)(2,3)   // 6
sum(1)(2)(3) // 6

该题其实就运用了函数柯里化的思想,我们可以将add方法通过函数柯里化转化为sum方法。

先来看看什么是函数柯里化。

什么是函数柯里化

维基百科中这样定义柯里化:

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

用大白话来说就是:只传递给函数一部分参数来调用它,让它返回一个新函数去处理剩下的参数。

举个例子理解柯里化:

假设你有一间商店,促销季,你想给普通顾客所有商品九折优惠。

1
2
3
function setDiscount(price, discount) {  
return price * discount;
}

当一个普通顾客买了一件价格为100元的商品,你计算价格可以:

1
const price = setDiscount(100, 0.9);

你会发现,每次计算一个商品价格,都需要输入价格和折扣,很麻烦。

1
2
3
const price1 = setDiscount(500, 0.9);
const price2 = setDiscount(1000, 0.9);
const price3 = setDiscount(2000, 0.9);

我们可以将函数setDiscount柯里化,避免每次都输入折扣。

1
2
3
4
5
6
7
function currySetDiscount(discount) {  
return function(price) {
return discount * price;
};
}

const setNinetyPercent = currySetDiscount(0.9);

现在我们可以这样计算普通顾客购买商品时的价格:

1
const price = setNinetyPercent(500);

同样的,针对VIP顾客,所有商品打八折,可以这样计算:

1
2
3
const setEightyPercent = currySetDiscount(0.8);

const price = setEightyPercent(500);

函数柯里化的这种作用,可以理解为参数复用,延迟执行,减少了代码冗余,增加了代码可读性

函数柯里化的简单实现

回到开始的那道题,我们来写一个curry函数。

1
2
3
4
5
6
7
8
9
10
11
12
function curry(fn, args) {  
let len = fn.length; // 待柯里化的函数的参数长度
let tempArgs = args || [];
return function() {
tempArgs = tempArgs.concat([...arguments])
if (tempArgs.length < len) {
return curry.call(this, fn, tempArgs);
} else {
return fn.apply(this, tempArgs);
}
};
}

原理:用闭包保存参数,当参数数量和原函数参数数量一致时,执行函数

可以这样来柯里化add,生成sum方法:

1
2
3
4
var sum = curry(add);

sum(1)(2,3) // 6
sum(1)(2)(3) // 6

函数柯里化的应用

延迟计算

我们常用的bind函数。

1
2
3
4
5
6
7
8
9
let obj = {
name: 'jack'
}
let showName = function() {
console.log(this.name)
}
let showJackName = showName.bind(obj);

showJackName(); // jack

这里bind用来改变函数执行时的上下文,但是函数本身并不执行,所以本质上是延迟计算。

我们看下bind的模拟实现,本质上就是一种柯里化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function myBind(){
var self = this;
// 第一个对象参数
var context = Array.prototype.shift.call(arguments);
// 其余参数
var bindArgs = Array.prototype.slice.call(arguments);
// 临时函数
var fTemp = function(){};
function fn(){
// 合并绑定参数以及调用时参数
var args = bindArgs.concat(Array.prototype.slice.call(arguments));
// 原函数执行(this指向给定对象)
self.apply(context, args);
}
// 临时函数prototype指向原函数prototype
fTemp.prototype = self.prototype;
// 新函数prototype设为临时函数的实例对象(当原函数使用New创建实例)
fn.prototype = new fTemp();
return fn;
}

总结

函数柯里化是函数式编程(一种编程范式)中的一个最基本的运算,它生于函数式编程,也主要服务于函数式编程(函数的组合前提是需要单参数的函数)。我们日常开发中其实无需特意区分是否使用函数柯里化,闭包,高阶函数等都一定程度上与函数柯里化有异曲同工之妙。