JavaScript 柯里化是前端职位面试中最常见的问题之一。熟练掌握它不仅可以帮你通过面试,获得更好的职业机会,更直接的作用是可以让你的代码更简洁,工作更高效。
如下是一个面试题:
请定义一个 JavaScript 方法,实现如下输出:sum(2, 3) // 5sum(2)(3) // 5
我们可以把问题分解,对于第一种参数是 2 个的情况,可以这样编写函数:
如上函数对于第一种情况是工作的,但如果执行第二种格式则会报错如下:
Uncaught TypeError: sum(...) is not a function
这是由于函数的返回值是一个数字,而不是执行 (3) 时所期待的函数。
我们在上述函数的基础上做一些改动,根据参数值的数组返回不同的类型。例如:
这里用到了 ES 6 的剩余参数特性(...args 和 ...args2)来保存传入的参数。
第 2 行:如果调用 sum 函数时传入的参数个数是 2,会直接返回这两个数字的和。第 5 行:否则会返回一个函数,以供后续的调用。第 6 行:在这个内嵌的函数中,会把之前调用 sum 时传入的参数 args 和本次调用 sum 函数时传入的 args2 合并为一个新的数组,然后使用 .apply() 方法调用 sum 函数,把合并后的数组作为参数传入。
仔细观察这个函数的具体实现方式之后,可以得出一个结论:它本质上是函数的递归调用。在参数个数未达到要求时(对于本例而言是 2 个参数),会返回一个新的函数,此函数调用时会调用外部定义的函数,当参数个数满足要求时返回期待的和,即一个数字。
延伸:更多参数的情况
上面我们讨论了求两数之和的情况,如果是更多数字之和呢?例如三数之和:
console.log(sum(2, 3, 4)) // 9console.log(sum(2)(3)(4)) // 9console.log(sum(2)(3, 4));// 9console.log(sum(2, 3)(4));// 9
聪明的你也许想到了可以改写上述的 sum 方法,把求和的判断条件对应调整:
在上述代码中第 2 行判断 args 参数的长度为 3 时,在第 3 行使用了数组的 reduce() 方法来计算数值之和。
问题出现了:如果参数个数不为 3,需要手动修改第 2 行中的数字 '3'。我们需要更智能的方式来处理这个问题。
柯里化函数的具体代码实现
上个小结中的 sum 函数已经有了柯里化的思维,不过有两个问题尚未解决:
在函数中需要预先得知参数的个数才能保证函数正常执行此方法不具有通用性,例如需要计算多个数字的乘积时仍然需要编写结构类似的函数,没有实现逻辑的复用。可以这样实现一个通用的 curry 方法:
使用方法如下:
curry 函数代码解释
第 1 行定义了 curry 方法的签名,它的入参是一个函数 fn第 2 行返回了一个函数,调用 curry(fn) 方法后会返回一个封装后 curried 函数,具体的逻辑是在此函数中定义并执行的。第 3 行判断如果调用 curried 函数时传入的参数个数和 fn 方法签名的参数个数一致,则把参数传入 fn 函数并执行,然后把它的返回值返回。这里使用 fn.length 获取 fn 方法的参数个数,解决了上述的第一个问题。第 6 行定义了一个匿名函数,适用于调用 curried 函数时参数个数少于期待值的情况。它会返回一个匿名函数,当再次调用时会在第 7 行使用 curried.apply() 方法尝试执行 curried 函数,这时逻辑会再次进入 curried 函数体,当最后一次调用参数个数达到期望值时,会执行第 4 行的代码,并最终返回。在理解了它的工作原理之后,我们发现柯里化只适用于参数个数固定的函数,它要求传入的函数的参数个数是已知的。所以如果传入的函数使用了剩余参数,则不生效。例如如下是不工作的:
在现实项目中的使用场景
在项目开发中合理使用柯里化可以让代码更加简洁明了和容易维护。如下是一个真实的例子。
假设你需要为 url-parse 这个 URL 解析库编写测试用例,确保它是正常工作的。我们可以使用 QUnit 这样编写测试用例:
上述代码可以正常工作,不过我们可以发现 URLParse(url1) 和 URLParse(url2) 出现了多次,特别是当检测的属性增多时会更加明显。
我们可以在此基础上封装一个方法,然后借助于 curry() 方法来简化代码:
只需要一点点额外的工作量,我们就大大减少了代码冗余,让编写和维护更加方便。
总结
柯里化是一种转换,将 f(a,b,c) 转换为可以被以 f(a)(b)(c) 的形式进行调用。JavaScript 实现通常都保持该函数可以被正常调用,并且如果参数数量不足,则返回匿名函数等待下次调用。
合理使用柯里化可以让代码更加简洁清晰,更加方便开发和维护。