JavaScript 柯里化


prtyaa
prtyaa 2023-12-26 17:55:22 62783
分类专栏: 资讯
  • JavaScript 柯里化是前端职位面试中最常见的问题之一。熟练掌握它不仅可以帮你通过面试,获得更好的职业机会,更直接的作用是可以让你的代码更简洁,工作更高效。

    如下是一个面试题:

    请定义一个 JavaScript 方法,实现如下输出:
    sum(2, 3) // 5
    sum(2)(3) // 5

    我们可以把问题分解,对于第一种参数是 2 个的情况,可以这样编写函数:

    function sum(a, b) {
      return a + b;
    }
    

    如上函数对于第一种情况是工作的,但如果执行第二种格式则会报错如下:

    Uncaught TypeError: sum(...) is not a function

    这是由于函数的返回值是一个数字,而不是执行 (3) 时所期待的函数。

    我们在上述函数的基础上做一些改动,根据参数值的数组返回不同的类型。例如:

    function sum(...args) {
      if (args.length === 2) {
        return args[0] + args[1];
      } else {
        return function(...args2) {
          return sum.apply(this, args.concat(args2));
        };
      }
    }
    sum(2)(3)
    

    这里用到了 ES 6 的剩余参数特性(...args 和 ...args2)来保存传入的参数。

    • 第 2 行:如果调用 sum 函数时传入的参数个数是 2,会直接返回这两个数字的和。
    • 第 5 行:否则会返回一个函数,以供后续的调用。
    • 第 6 行:在这个内嵌的函数中,会把之前调用 sum 时传入的参数 args 和本次调用 sum 函数时传入的 args2 合并为一个新的数组,然后使用 .apply() 方法调用 sum 函数,把合并后的数组作为参数传入。

    仔细观察这个函数的具体实现方式之后,可以得出一个结论:它本质上是函数的递归调用。在参数个数未达到要求时(对于本例而言是 2 个参数),会返回一个新的函数,此函数调用时会调用外部定义的函数,当参数个数满足要求时返回期待的和,即一个数字。

    延伸:更多参数的情况

    上面我们讨论了求两数之和的情况,如果是更多数字之和呢?例如三数之和:

    console.log(sum(2, 3, 4)) // 9
    console.log(sum(2)(3)(4)) // 9
    console.log(sum(2)(3, 4));// 9
    console.log(sum(2, 3)(4));// 9
    

    聪明的你也许想到了可以改写上述的 sum 方法,把求和的判断条件对应调整:

    function sum(...args) {
      if (args.length === 3) {
        return args.reduce((prev, cur) => prev + cur, 0);
      } else {
        return function(...args2) {
          return sum.apply(this, args.concat(args2));
        };
      }
    }
    

    在上述代码中第 2 行判断 args 参数的长度为 3 时,在第 3 行使用了数组的 reduce() 方法来计算数值之和。

    问题出现了:如果参数个数不为 3,需要手动修改第 2 行中的数字 '3'。我们需要更智能的方式来处理这个问题。

    柯里化函数的具体代码实现

    上个小结中的 sum 函数已经有了柯里化的思维,不过有两个问题尚未解决:

    • 在函数中需要预先得知参数的个数才能保证函数正常执行
    • 此方法不具有通用性,例如需要计算多个数字的乘积时仍然需要编写结构类似的函数,没有实现逻辑的复用。

    可以这样实现一个通用的 curry 方法:

    function curry(fn) {
      return function curried(...args) {
        if (args.length === fn.length) {
          return fn.apply(this, args);
        } else {
          return function(...args2) {
            return curried.apply(this, args.concat(args2));
          };
        }
      };
    }
    

    使用方法如下:

    const sum = curry(function(a, b, c, d) {
      return a + b + c + d;
    });
    console.log(sum(2,3,4,5));
    console.log(sum(2)(3)(4)(5));
    console.log(sum(2)(3,4,5));
    console.log(sum(2,3)(4,5));
    console.log(sum(2,3,4)(5));
    

    curry 函数代码解释

    • 第 1 行定义了 curry 方法的签名,它的入参是一个函数 fn
    • 第 2 行返回了一个函数,调用 curry(fn) 方法后会返回一个封装后 curried 函数,具体的逻辑是在此函数中定义并执行的。
    • 第 3 行判断如果调用 curried 函数时传入的参数个数和 fn 方法签名的参数个数一致,则把参数传入 fn 函数并执行,然后把它的返回值返回。这里使用 fn.length 获取 fn 方法的参数个数,解决了上述的第一个问题。
    • 第 6 行定义了一个匿名函数,适用于调用 curried 函数时参数个数少于期待值的情况。它会返回一个匿名函数,当再次调用时会在第 7 行使用 curried.apply() 方法尝试执行 curried 函数,这时逻辑会再次进入 curried 函数体,当最后一次调用参数个数达到期望值时,会执行第 4 行的代码,并最终返回。

    在理解了它的工作原理之后,我们发现柯里化只适用于参数个数固定的函数,它要求传入的函数的参数个数是已知的。所以如果传入的函数使用了剩余参数,则不生效。例如如下是不工作的:

    // 传入的函数使用了 rest parameters
    const sum = curry(function(...nums) {
      return nums.reduce((prev, cur) => prev + cur, 0);
    });
    // 如下都会输出 f(...args2) 函数,而不是预期的数字之和
    console.log(sum(2,3,4,5));
    console.log(sum(2)(3)(4)(5));
    console.log(sum(2)(3,4,5));
    console.log(sum(2,3)(4,5));
    console.log(sum(2,3,4)(5));
    

    在现实项目中的使用场景

    在项目开发中合理使用柯里化可以让代码更加简洁明了和容易维护。如下是一个真实的例子。

    假设你需要为 url-parse 这个 URL 解析库编写测试用例,确保它是正常工作的。我们可以使用 QUnit 这样编写测试用例:

    上述代码可以正常工作,不过我们可以发现 URLParse(url1) 和 URLParse(url2) 出现了多次,特别是当检测的属性增多时会更加明显。

    我们可以在此基础上封装一个方法,然后借助于 curry() 方法来简化代码:

    只需要一点点额外的工作量,我们就大大减少了代码冗余,让编写和维护更加方便。

    总结

    柯里化是一种转换,将 f(a,b,c) 转换为可以被以 f(a)(b)(c) 的形式进行调用。JavaScript 实现通常都保持该函数可以被正常调用,并且如果参数数量不足,则返回匿名函数等待下次调用。

    合理使用柯里化可以让代码更加简洁清晰,更加方便开发和维护。

网站声明:如果转载,请联系本站管理员。否则一切后果自行承担。

本文链接:https://www.xckfsq.com/news/show.html?id=30950
赞同 0
评论 0 条