JavaScript内核与高级编程之:`JavaScript`的`Currying`:其在函数组合和参数复用中的应用。

嘿,各位编程界的弄潮儿们,很高兴能在这里和大家聊聊 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 的实现方式有很多种,这里介绍几种常见的:

  1. 手动 Currying: 就像上面的例子一样,手动编写嵌套的函数。虽然直观,但是对于参数多的函数,代码会变得非常冗长。

  2. 通用 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 合并在一起。
  3. 利用 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 有更深入的了解。记住,编程的道路永无止境,不断学习和实践才是王道。下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注