一文搞懂Javascript中的函数柯里化(currying)

914 阅读7分钟

1. 什么是柯里化

在使用JavaScript编写代码的时候,有一种函数式编程的思想,而提到函数式编程,一定绕不开一个概念,那就是柯里化。柯里化是编程语言中的一个通用的概念(不只是Js,其他很多语言也有柯里化),是指把接收多个参数的函数变换成接收单一参数的函数,嵌套返回直到所有参数都被使用并返回最终结果。更简单地说,柯里化是一个函数变换的过程,是将函数从调用方式:f(a,b,c)变换成调用方式:f(a)(b)(c)的过程。柯里化不会调用函数,它只是对函数进行转换。

柯里化是一个把具有较多 arity 的函数转换成具有较少 arity 函数的过程 -- Kristina Brainwave

下面先看一个最简单的例子,就能更加直观的认识什么是柯里化。

实现一个求三个数的加和的函数:

function addThreeNum (a, b, c) {
	return a + b + c;
}

非常简单的一个函数,接收三个参数,返回最终的加和值

addTreeNum(6, 9 ,10);// 返回结果为25

下面对addThreeNume进行柯里化

function addhTreeNumCurry(a) {
	return function(b) {
		return function(c) {
			return a + b + c;
		}
	}
}

新的调用方式:

addThreeNumCurry(6)(9)(10);// 返回结果同样是25

// 分部调用柯里化后的函数
const add1 = addThreeNumCurry(6);// 返回的是一个函数
const add2 = add1(9);// 返回的是一个函数
const add3 = add2(10);// 已接收到所有的参数,返回最终的计算结果

console.log(add3);// 25

在以上过程中,柯里化后的加和函数,每次都是传入单个参数,返回的函数都会保留之前传入的所有参数,并在最后一个函数传入后进行最终的计算。这就等于说,函数一直保留着之前的所有状态,等到所有条件都满足后,执行最终的操作。

2.柯里化的用处

理解了柯里化的基本概念和用法,那么下面讲讲柯里化在实际场景中的用处:

(1) 延迟计算

(2) 参数复用

这两个场景非常好理解,柯里化函数将在接收到最后一个参数的时候才进行最后的计算,与普通一次性接收所有参数的函数相比,延迟了最终计算,并且,前面传入的参数还可以被后续的调用所复用。

假设你是一个商家,要出售商品,为了卖出去更多的商品,今天决定打9折进行售卖,我们可以使用以下函数进行折扣后的售出价格计算:

function discount(price, discount) {
	return price * (1 - discount);// discount为小数,例如0.1代表优惠10%
}

当一个用户买了一件5000元的商品,那么你收到的钱就是:

const price = discount(5000, 0.1);// = 5000 * (1 - 0.1) = 4500

当有一个顾客购买商品就会使用0.1的折扣去调用一次discount方法,那么当有很多顾客的时候,就会每次都变化discount的第一个参数,而第二个参数就一直重复一样为0.1。这里的参数一直重复一样,其实是可以优化的,我们通过对这个函数进行柯里化来进行一次优化:

// 柯里化上面的discount函数
function discountCurry(discount) {
	return function(price) {
		return price * (1 - discount);
	}
}

// 这样我们只需要先设定一个折扣
const tenPercentDiscount = discountCurry(0.1);// 设定一个10%的优化价格
// 接下来只需要对每一个商品的单价传入进行计算即可得到对应的折扣后的价格
const goodPrice1 = tenPercentDiscount(5000);// 4500
const goodPrice2 = tenPercentDiscount(1000);// 900
const goodPrice3 = tenPercentDiscount(3000);// 2700

上面的分步调用会让我们对与整个代码逻辑更加清晰,接下来我们还可以进步扩展到,我们可以动态设置折扣力度,假设第二天你需要加大折扣力度,变成优惠30%,那么直接调用这个柯里化后的discountCurry函数进行折扣设置,然后再去计算没意见商品的价格即可:

const thirtyPercentDiscount = discountCurry(0.3);// 设置一个7折的折扣力度

// 计算商品的售价
const price = thirtyPercentDiscount(5000);// 3500

(3) 动态生成函数

这里举一个实际例子。我们都知道为了兼容IE和其他浏览器的添加事件方法,通常会以下面代码进行兼容行处理:

const addEvent = (ele, type, fn, capture) => {
	if (window.addEventListener) {
		ele.addEventListener(type, (e) => fn.call(ele, e), capture);
	} else if (window.attachEvent) {
		ele.attachEvent('on'+type, (e) => fn.call(ele, e));
	}
}

这里会有一个问题,就是在每一次绑定事件的时候,都需要一次环境的判断,再去进行绑定,如果我们将上面的函数进行柯里化,就能规避这个问题,在使用前是做一次判断即可。

const addEvent = (function() {
	if (window.addEventListener) {
		return function(ele) {
			return function(type) {
				return function(fn) {
					return function(capture) {
						ele.addEventListener(type, (e) => fn.call(ele, e), capture);
					}
				}
			}
		}
	} else if (window.attachEvent) {
		return function(ele) {
			return function(type) {
				return function(fn) {
					return function(capture) {
						ele.addEventListener(type, (e) => fn.call(ele, e), capture);
					}
				}
			}
		}
	}
})();

// 调用
addEvent(document.getElementById('app'))('click')((e) => {console.log('click function has been call:', e);})(false);

// 分步骤调用会更加清晰
const ele = document.getElementById('app');
// get environment
const environment = addEvent(ele)
// bind event
environment('click')((e) => {console.log(e)})(false);

上面例子虽然显得有点绕,但利用柯里化的函数可以实现动态的生成不同的函数。实际场景这些代码还可以进行优化

利用以上三种柯里化带来的好处,我们可以在实际代码中运用更多柯里化的方式去解决问题。

4.将普通函数柯里化的函数

通过上面的内容,我们知道了什么是柯里化和柯里化的应用,那么更进一步,思考一下如何把一个普通函数转换成柯里化的函数,我们可以使用一个特定的函数去专门做这件事情,那我们就可以非常轻松的把普通函数转换成柯里化后的函数了。

这个神奇的可以将普通函数柯里化的函数实现:

// 柯里化函数
const curry = function (fn) {
    return function nest(...args) {
        // fn.length表示函数的形参个数
        if (args.length === fn.length) {
            // 当参数接收的数量达到了函数fn的形参个数,即所有参数已经都接收完毕则进行最终的调用
            return fn(...args);
        } else {
            // 参数还未完全接收完毕,递归返回judge,将新的参数传入
            return function (arg) {
                return nest(...args, arg);
            }
        }
    }
}

接着来看调用的过程:

function addNum(a, b, c) {
    return a + b + c;
}

const addCurry = curry(addNum);

addCurry(1)(2)(3);// 6

详细剖析一下curry函数被调用的整个过程是如何生成柯里化的函数并最终调用的时候得到最终的结果的:

const addCurry = curry(addNum);
// 这一步得到了内部返回nest函数

// ƒ judge(...args) {
//         console.log('args:', args);
//         console.log('fn.length:', fn.length);
//         if (args.length === fn.length) {
//             return fn(...args);
//         } else {
//           …

addCurry(1)(2)(3);

// 这个过程分为三步
// step1:
// addCurry(1)
// 返回下面的函数
// ƒ (arg) {
// 	return judge(1, arg);
// }
// step2:
// addCurry(1)(2)
// 返回下面的函数
// ƒ (arg) {
// 	return judge(1,2, arg);
// }
// step3:
// addCurry(1)(2)(3)
// 返回并执行下面的函数
// return fn(1,2,3);
// 最终得到结果6

以上就是整个curry函数的应用和执行过程,如果这个过程还有不明白的地方,可以自己手动敲一遍,在浏览器里运行着每一步打印出来结果就可以非常清楚整个过程了。

5.总结

本文主要介绍说明函数的柯里化以及在Js编程中的实际应用。个人觉得柯里化是一个在写代码的时候的一种思想,一种指导方法论,它可以出现在任何可以用得到的地方,将这种解决问题的思想带入到平时的代码使用中,势必会增加一条更加好的解决问题的思路。希望通过此篇文章,可以让你搞懂柯里化。

参考: