嘿,各位编程界的弄潮儿们,很高兴能在这里和大家聊聊 JavaScript 里一个既古老又时髦的概念——Currying (柯里化)。别被这个听起来高大上的名字吓跑,其实它就是把一个接受多个参数的函数,变成一系列接受单个参数的函数的过程。
今天,咱们就来扒一扒 Currying 的底裤,看看它到底有什么魔力,能在函数组合和参数复用中大显身手。准备好了吗?咱们开始了!
第一幕:Currying 是个啥?
想象一下,你是一位大厨,要做一道“葱油拌面”。你需要葱、油、面条、酱油等等一堆食材。
-
普通函数: 你一股脑把所有食材都交给厨师,厨师一次性把面做出来。
function makeNoodles(onion, oil, noodles, soySauce) { return `一份美味的葱油拌面,用了 ${onion} 葱,${oil} 油,${noodles} 面条,${soySauce} 酱油。`; } const myNoodles = makeNoodles("小葱", "香油", "细面", "生抽"); console.log(myNoodles); // 输出: 一份美味的葱油拌面,用了 小葱 葱,香油 油,细面 面条,生抽 酱油。
-
Currying 函数: 你每次只给厨师一种食材,厨师每次都返回一个“半成品”函数,直到你给完所有食材,厨师才把最终的面做出来。
function curryMakeNoodles(onion) { return function(oil) { return function(noodles) { return function(soySauce) { return `一份美味的葱油拌面,用了 ${onion} 葱,${oil} 油,${noodles} 面条,${soySauce} 酱油。`; } } } } const noodlesWithOnion = curryMakeNoodles("小葱"); const noodlesWithOnionAndOil = noodlesWithOnion("香油"); const noodlesWithOnionOilAndNoodles = noodlesWithOnionAndOil("细面"); const myCurriedNoodles = noodlesWithOnionOilAndNoodles("生抽"); console.log(myCurriedNoodles); // 输出: 一份美味的葱油拌面,用了 小葱 葱,香油 油,细面 面条,生抽 酱油。 // 或者更简洁的写法 const myCurriedNoodlesShort = curryMakeNoodles("小葱")("香油")("细面")("生抽"); console.log(myCurriedNoodlesShort); // 输出: 一份美味的葱油拌面,用了 小葱 葱,香油 油,细面 面条,生抽 酱油。
这就是 Currying 的核心思想:把一个多参数函数拆解成一系列单参数函数,每个函数都返回一个新的函数,直到所有参数都被传入。
第二幕:Currying 的实现方式
Currying 的实现方式有很多种,这里介绍几种常见的:
-
手动 Currying: 就像上面的例子一样,手动编写嵌套的函数。虽然直观,但是对于参数多的函数,代码会变得非常冗长。
-
通用 Currying 函数: 编写一个通用的 Currying 函数,可以自动将任何多参数函数转换成 Currying 函数。
function curry(fn) { const arity = fn.length; // 获取函数的参数个数 return function curried(...args) { if (args.length >= arity) { return fn.apply(this, args); // 参数足够,执行原函数 } else { return function(...newArgs) { return curried.apply(this, args.concat(newArgs)); // 参数不足,返回新的 curried 函数 } } }; } function add(a, b, c) { return a + b + c; } const curriedAdd = curry(add); console.log(curriedAdd(1)(2)(3)); // 输出: 6 console.log(curriedAdd(1, 2)(3)); // 输出: 6 console.log(curriedAdd(1)(2, 3)); // 输出: 6 console.log(curriedAdd(1, 2, 3)); // 输出: 6
这个
curry
函数的工作原理是:- 首先,它获取目标函数
fn
的参数个数arity
。 - 然后,它返回一个
curried
函数。 curried
函数接收任意数量的参数...args
。- 如果
args
的长度大于等于arity
,说明参数已经足够,就用apply
调用原函数fn
,并传入所有参数。 - 如果
args
的长度小于arity
,说明参数还不够,就返回一个新的函数,这个函数接收新的参数...newArgs
,然后递归调用curried
函数,并将之前的参数args
和新的参数newArgs
合并在一起。
- 首先,它获取目标函数
-
利用 Lodash/Ramda 等库: 这些库提供了现成的
curry
函数,可以直接使用,省去了自己编写的麻烦。// 使用 Lodash const _ = require('lodash'); function multiply(a, b, c) { return a * b * c; } const curriedMultiply = _.curry(multiply); console.log(curriedMultiply(2)(3)(4)); // 输出: 24 // 使用 Ramda const R = require('ramda'); const divide = (a, b) => a / b; const curriedDivide = R.curry(divide); const halfOf = curriedDivide(2); // 创建一个新函数,用于计算一半的值 console.log(halfOf(10)); // 输出: 5
使用库的好处是代码更简洁,而且库通常会对性能进行优化。
第三幕:Currying 在函数组合中的应用
函数组合是将多个函数组合成一个新函数的过程。Currying 在函数组合中扮演着重要的角色,它可以让函数组合更加灵活和易于理解。
假设我们有以下几个函数:
function add5(x) {
return x + 5;
}
function multiplyBy2(x) {
return x * 2;
}
function square(x) {
return x * x;
}
我们想要创建一个新函数,它先将一个数加 5,然后乘以 2,最后平方。
-
不使用 Currying 的函数组合:
function compose(f, g, h) { return function(x) { return h(g(f(x))); } } const composedFunction = compose(add5, multiplyBy2, square); console.log(composedFunction(3)); // 输出: 64 ((3 + 5) * 2)^2 = 64
这种方式虽然可行,但是可读性较差,而且参数的顺序容易搞混。
-
使用 Currying 的函数组合:
function compose(...fns) { return function(x) { return fns.reduceRight((acc, fn) => fn(acc), x); } } const curriedAdd5 = curry(add5); const curriedMultiplyBy2 = curry(multiplyBy2); const curriedSquare = curry(square); const composedFunctionWithCurrying = compose(curriedAdd5, curriedMultiplyBy2, curriedSquare); console.log(composedFunctionWithCurrying(3)); // 输出: 64
或者,更简洁的使用 Ramda 库:
const R = require('ramda'); const composedFunctionRamda = R.compose(square, multiplyBy2, add5); console.log(composedFunctionRamda(3)); // 输出: 64
使用 Currying 的好处是:
- 代码更清晰: 每个函数只接受一个参数,更容易理解函数的作用。
- 更灵活: 可以根据需要组合任意数量的函数。
- 可复用性更高: Currying 后的函数可以单独使用,也可以与其他函数组合使用。
第四幕:Currying 在参数复用中的应用
Currying 的另一个重要应用是参数复用。通过 Currying,我们可以预先设置一些参数,然后生成一个新的函数,这个新函数只需要接受剩余的参数即可。
例如,假设我们需要一个函数来计算折扣后的价格。
function applyDiscount(discount, price) {
return price * (1 - discount);
}
如果我们经常需要计算 8 折的价格,可以这样做:
const calculate80Percent = curry(applyDiscount)(0.2); // 预先设置 discount 为 0.2
console.log(calculate80Percent(100)); // 输出: 80
console.log(calculate80Percent(200)); // 输出: 160
通过 Currying,我们创建了一个新的函数 calculate80Percent
,它只需要接受价格作为参数,而折扣已经预先设置好了。这大大提高了代码的复用性。
再来一个例子,假设我们需要一个函数来生成特定格式的 URL。
function generateURL(protocol, domain, path) {
return `${protocol}://${domain}/${path}`;
}
我们可以使用 Currying 来创建一个生成 HTTPS URL 的函数:
const generateHTTPSURL = curry(generateURL)("https");
console.log(generateHTTPSURL("example.com", "users")); // 输出: https://example.com/users
或者,创建一个生成特定域名下 URL 的函数:
const generateExampleURL = curry(generateURL)("https", "example.com");
console.log(generateExampleURL("users")); // 输出: https://example.com/users
第五幕:Currying 的优缺点
任何技术都有优缺点,Currying 也不例外。
优点 | 缺点 |
---|---|
提高代码的可读性和可维护性 | 增加了代码的复杂性,特别是手动 Currying 时 |
提高代码的复用性 | 可能导致性能下降,因为需要多次函数调用 |
方便函数组合 | 需要额外的学习成本 |
可以实现延迟执行(参数不全时不会立即执行) | 过度使用 Currying 会使代码难以理解 |
第六幕:总结与思考
Currying 是一种强大的函数式编程技巧,它可以让我们的代码更简洁、更灵活、更易于复用。虽然 Currying 有一些缺点,但是只要合理使用,就可以发挥它的优势,提高我们的编程效率。
希望今天的讲座能让你对 Currying 有更深入的了解。记住,编程的道路永无止境,不断学习和实践才是王道。下次再见!