探讨 `JavaScript` 中的 `Currying` (柯里化) 和 `Partial Application` (偏应用) 的区别和实际应用。

好的,各位听众朋友们,早上好/下午好/晚上好! 今天咱们来聊聊 JavaScript 里两个听起来高大上,但其实挺接地气的概念:Currying(柯里化)和 Partial Application(偏应用)。 别被这俩名字唬住,它们说白了就是函数玩的花样,能让你的代码更灵活,更易读,更有…装逼范儿(开玩笑)。

开场白:函数界的变形金刚

想象一下,函数就像变形金刚,平时是个函数,关键时刻能变身成其他函数,或者只露一部分真身出来。柯里化和偏应用就是让函数变形的两种常用方式。

第一部分:Currying (柯里化) – “化整为零”的艺术

  • 什么是柯里化?

    简单来说,柯里化就是把一个接受多个参数的函数,变成一系列只接受一个参数的函数的过程。 每个函数都返回一个新的函数,直到所有参数都被传入为止,最后返回结果。

    用大白话说: 原本你需要一次性喂给函数一大堆参数,柯里化后,你可以一个一个喂,每次喂完它都记住你喂的啥,直到喂饱为止。

  • 柯里化的过程

    假设我们有这样一个加法函数:

    function add(x, y, z) {
      return x + y + z;
    }

    柯里化后,它会变成这样:

    function curriedAdd(x) {
      return function(y) {
        return function(z) {
          return x + y + z;
        }
      }
    }
    
    // 使用柯里化后的函数
    const add5 = curriedAdd(5); // 返回一个等待 y 的函数
    const add5and6 = add5(6);   // 返回一个等待 z 的函数
    const result = add5and6(7);   // 返回最终结果:18
    console.log(result); // 输出 18

    或者更简洁一点:

    const curriedAdd = x => y => z => x + y + z;
    
    // 使用柯里化后的函数
    const add5 = curriedAdd(5);
    const add5and6 = add5(6);
    const result = add5and6(7);
    console.log(result); // 输出 18
  • 柯里化的通用实现

    上面的例子是针对特定函数的柯里化。 我们可以写一个通用的柯里化函数,让任何函数都能被柯里化:

    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));
          }
        }
      };
    }
    
    // 使用通用的 curry 函数
    const add = (x, y, z) => x + y + z;
    const curriedAdd = curry(add);
    
    const add5 = curriedAdd(5);
    const add5and6 = add5(6);
    const result = add5and6(7);
    console.log(result); // 输出 18
    
    // 也可以一次性传入所有参数
    const result2 = curriedAdd(1, 2, 3);
    console.log(result2); // 输出 6

    代码解释:

    • curry(fn): 接受一个函数 fn 作为参数,返回一个新的柯里化后的函数。
    • arity = fn.length: 获取函数 fn 的参数个数,这很重要,因为我们需要知道什么时候参数已经全部传入。
    • curried(...args): 返回的柯里化函数,使用剩余参数语法 ...args 收集传入的参数。
    • if (args.length >= arity): 如果传入的参数个数大于等于函数 fn 的参数个数,就说明所有参数都传入了,直接调用 fn 并返回结果。 使用 fn.apply(this, args) 可以确保函数在正确的上下文中执行,并将参数传递给它。
    • else: 如果传入的参数个数小于函数 fn 的参数个数,就返回一个新的函数,这个新函数会继续收集参数,直到所有参数都传入为止。
    • curried.apply(this, args.concat(newArgs)): 将之前传入的参数 args 和新传入的参数 newArgs 合并,然后递归调用 curried 函数。
  • 柯里化的优势

    • 延迟执行 (Lazy Evaluation): 参数可以一个一个传入,函数不会立即执行,直到所有参数都传入后才会执行。
    • 代码复用: 可以创建一些预设了部分参数的新函数,方便在不同的场景中使用。
    • 函数组合 (Function Composition): 柯里化是函数式编程的基础,方便进行函数组合。
  • 柯里化的应用场景

    • 事件处理: 可以预先设置一些事件处理函数的参数。
    • 配置管理: 可以预先设置一些配置参数,然后在不同的模块中使用。
    • 数据验证: 可以创建一些预设了验证规则的验证函数。

第二部分:Partial Application (偏应用) – “先喂一半”的策略

  • 什么是偏应用?

    偏应用是指创建一个新的函数,这个新函数预先绑定了原函数的部分参数。 当调用这个新函数时,只需要传入剩余的参数即可。

    用大白话说: 原本你需要一次性喂给函数一大堆参数,偏应用允许你先喂一部分,得到一个已经记住这些参数的新函数,以后再喂剩下的。

  • 偏应用的过程

    继续使用上面的加法函数:

    function add(x, y, z) {
      return x + y + z;
    }

    偏应用后,我们可以创建一个预先绑定了 xy 的新函数:

    function partialAdd(x, y) {
      return function(z) {
        return add(x, y, z);
      }
    }
    
    // 使用偏应用后的函数
    const add5and6 = partialAdd(5, 6); // 返回一个等待 z 的函数
    const result = add5and6(7);   // 返回最终结果:18
    console.log(result); // 输出 18
  • 偏应用的通用实现

    function partial(fn, ...presetArgs) {
      return function(...laterArgs) {
        return fn.apply(this, presetArgs.concat(laterArgs));
      }
    }
    
    // 使用通用的 partial 函数
    const add = (x, y, z) => x + y + z;
    const add5and6 = partial(add, 5, 6);
    
    const result = add5and6(7);
    console.log(result); // 输出 18

    代码解释:

    • partial(fn, ...presetArgs): 接受一个函数 fn 和一些预设参数 ...presetArgs 作为参数,返回一个新的函数。
    • (...laterArgs): 返回的新函数,使用剩余参数语法 ...laterArgs 收集之后传入的参数。
    • fn.apply(this, presetArgs.concat(laterArgs)): 调用原函数 fn,并将预设参数 presetArgs 和之后传入的参数 laterArgs 合并后传递给它。 使用 fn.apply(this, ...) 可以确保函数在正确的上下文中执行。
  • 偏应用的优势

    • 代码复用: 可以创建一些预设了部分参数的新函数,方便在不同的场景中使用。
    • 提高可读性: 可以将一些常用的参数预先绑定,使代码更易读懂。
    • 简化函数调用: 可以减少函数调用时需要传入的参数个数。
  • 偏应用的例子

    假设我们有一个日志函数:

    function log(level, message) {
      console.log(`[${level}] ${message}`);
    }

    我们可以使用偏应用创建一个专门记录错误日志的函数:

    const errorLog = partial(log, 'ERROR');
    
    errorLog('Something went wrong!'); // 输出:[ERROR] Something went wrong!

第三部分:Currying vs Partial Application – 傻傻分不清?

很多人容易把柯里化和偏应用搞混,因为它们都涉及到预先设置函数的部分参数。 但它们之间还是有明显的区别的:

特性 Currying (柯里化) Partial Application (偏应用)
参数处理 每次只接受一个参数,返回一个接受下一个参数的函数。 可以一次接受多个参数,返回一个接受剩余参数的函数。
返回值类型 总是返回一个函数,直到所有参数都传入后才返回最终结果。 返回一个函数,这个函数接受剩余的参数并调用原函数。
参数应用顺序 必须按照参数的顺序依次传入。 可以一次性传入多个参数,不一定需要按照参数的顺序。
最终结果产生时间 只有在所有参数都传入后才会产生最终结果。 可以在部分参数传入后立即产生结果,也可以等待剩余参数传入后再产生结果。

总结:

  • 柯里化 就像是把一个大饼切成一片一片的,每次吃一片,直到吃完整个饼。
  • 偏应用 就像是先在饼上抹了一层酱,以后吃的时候只需要再加点料就行了。

举个更形象的例子:

假设你要做一道菜:糖醋排骨。

  • 柯里化 就像是把做糖醋排骨的步骤分解成: 1. 买排骨, 2. 焯水, 3. 炸排骨, 4. 调糖醋汁, 5. 熬汁, 6. 倒入排骨。 你必须按照这个顺序一步一步来,每一步都产生一个新的“状态”,直到所有步骤都完成,才能做出糖醋排骨。

  • 偏应用 就像是提前把糖醋汁调好(预设了糖、醋、酱油等参数),以后做糖醋排骨的时候,只需要把排骨炸好,然后倒入糖醋汁熬一下就可以了。 你提前完成了部分步骤,减少了后续的步骤。

第四部分:实战演练 – 让代码更优雅

  • 数据验证

    假设我们有一个验证函数,用于验证用户的年龄是否在指定范围内:

    function isAgeValid(min, max, age) {
      return age >= min && age <= max;
    }

    使用柯里化:

    const curriedIsAgeValid = curry(isAgeValid);
    const isTeenager = curriedIsAgeValid(13)(19); // 创建一个验证是否为青少年的函数
    
    console.log(isTeenager(15)); // 输出 true
    console.log(isTeenager(20)); // 输出 false

    使用偏应用:

    const isTeenager = partial(isAgeValid, 13, 19); // 创建一个验证是否为青少年的函数
    
    console.log(isTeenager(15)); // 输出 true
    console.log(isTeenager(20)); // 输出 false
  • 事件处理

    假设我们有一个按钮,点击按钮后需要发送一个请求到服务器,并显示响应结果:

    <button id="myButton">Click me!</button>
    function fetchData(url, data, callback) {
      // 模拟发送请求
      setTimeout(() => {
        const response = `Response from ${url} with data: ${JSON.stringify(data)}`;
        callback(response);
      }, 1000);
    }
    
    function showResult(result) {
      alert(result);
    }
    
    const myButton = document.getElementById('myButton');
    
    // 使用偏应用预先设置 URL
    const fetchDataFromAPI = partial(fetchData, '/api/data');
    
    myButton.addEventListener('click', () => {
      fetchDataFromAPI({ id: 123 }, showResult);
    });

    在这个例子中,我们使用偏应用预先设置了 fetchData 函数的 url 参数,这样在事件处理函数中只需要传入 datacallback 即可,使代码更简洁易懂。

  • 日志记录

    function log(level, logger, message) {
        logger(`[${level}] ${message}`);
    }
    
    const consoleLog = partial(log, 'INFO', console.log.bind(console));
    const consoleWarn = partial(log, 'WARN', console.warn.bind(console));
    const consoleError = partial(log, 'ERROR', console.error.bind(console));
    
    consoleLog('Application started');
    consoleWarn('Possible issue detected');
    consoleError('Something went wrong');

第五部分:总结与展望

柯里化和偏应用是 JavaScript 中非常有用的函数式编程技巧。 它们可以提高代码的灵活性、可读性和可复用性。 虽然刚开始可能有点难以理解,但只要多加练习,就能掌握它们,并在实际开发中灵活运用。

记住,编程就像做菜,柯里化和偏应用就像是切菜和调料,用好了能让你的菜肴更美味(代码更优雅)。

今天的讲座就到这里,谢谢大家! 希望大家有所收获,并在实际开发中多多尝试使用柯里化和偏应用,让你的代码更上一层楼!

发表回复

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