壹.3.2 JavaScript函数柯里化

第一次看到柯里化这个词的时候,还是在2007年看一篇算法相关的博客提到把函数柯里化,那时一看这个词就感觉很高端,实际上了解了后才发现其实就是高阶函数的一个特殊用法。

柯里化的定义

柯里化(Currying)是一种技术,它把接受m个参数的函数变成接受n个参数的函数(0<n<m),并且该函数返回一个新函数,这个新函数接受余下的参数……如此循环,直到最后返回结果。

看这个定义可能有一点抽象,我们就来看一个简单的示例。

// 普通的add函数
function add(x, y) {
    return x + y
}

// Currying后
function curryingAdd(x) {
    return function (y) {
        return x + y
    }
}

add(1, 2)           // 3
curryingAdd(1)(2)   // 3

实际上就是把add函数的x,y两个参数变成了先用一个函数接收x然后返回一个函数去处理y参数。

那么,柯里化额外的封装了一层有什么具体的好处呢?

柯里化的好处

1. 复用参数

上面的示例是一个正则的校验,正常来说直接调用check函数就可以了,但是如果我们有很多地方都要校验是否有数字,第一个参数其实没有变化,变化的是第二个参数。Currying之后,第一个参数reg的输入就可以省略掉,后面敲代码更省事了。

2. 延迟运行

先仍然看一个add函数代码:

上面代码让函数通过柯里化,判断参数,如果有参数,就保存起来,不执行求和,直到最后一步才真正执行求和运算,达到了延迟运行的效果。

读者可能会问,延迟运行一定就要柯里化吗?当然不是。延迟运行和柯里化没有必然联系,本质上延迟运行,函数返回一个新函数(闭包)就可以实现了,这里只是举例说明柯里化有这个特性而已。

顺带提一下,代码中用到了ES6的reducerest运算符,对阅读理解可能有点影响,但熟悉语法运用之后,就会发现这些语法糖写出来的代码很简洁。

另外,js中经常使用的bind,实现的机制就是Currying。bind 用来改变函数执行时候的this,但是函数本身并不执行,所以是延迟运行,这一点和call / apply直接执行有所不同。

3.参与科学计算

援引百度百科的解释:

理论计算机科学arrow-up-right中,柯里化提供了一个办法,可以在简单的理论模型中,比如只接受一个单一参数的lambda演算中研究带有多个参数的函数。

通用的封装

给定一个函数fn,设计一个通用封装(currying函数)作用于这个fn,让这个fn可以支持柯里化,该怎么实现呢?思路:

  • 要让fn(a,b)等价于fn(a)(b),那么fn(a)要返回一个函数,这个函数接受参数b,并返回与fn(a,b)相同的结果。

  • 设计一个currying函数,它接受第一个参数是要被柯里化的函数fn,第2~N个参数是原本要传递给fn的参数(这个可用rest操作符实现)。

  • 柯里化主要围绕“处理参数”思考,不管怎么变化传参形式,柯里化之后的函数要把之前函数的参数都统统处理完毕才算合格。

代码如下:

看上面代码示例,已经有点柯里化了,但是还不符合要求。因为这个代码柯里化后得到的新函数add1只支持传“1段”参数,如果连续多段地传参数比如curry(a)(b)这样的话就不支持了。

circle-info

笔者注:函数右边带的一个可以处理参数的小括号对(),本书称为“1段” 。函数若能多带1段,我们称函数有多1段的能力。

代码如下:

看上面代码第7行报错部分,是因为curry(1)没有返回函数,怎么办呢?若参数没处理完,想办法返回函数就行了。一般这种情况可以用递归再封装一层。我们可以把这个primaryCurrying函数用作辅助函数(因为它已支持1段以便处理参数了),帮助我们写真正的 柯里化函数。

这边其实是在初步的基础上,加上了递归的调用,只要原本要传给fn的参数还未传完,就会继续执行递归。来测试一下效果怎么样?

带占位符的封装

占位符可以支持不按顺序传递参数,举例要求如下:

这里给出一个比较强大的代码实现:

柯里化性能

Currying的一些性能问题,笔者总结目前主要是以下四点:

  • 一些实现基于存取arguments对象,通常要比存取命名参数慢一点;

  • 一些老版本的浏览器在arguments.length的实现上是相当慢的;

  • 使用fn.apply( … ) 和 fn.call( … )通常比直接调用fn( … ) 稍微慢点;

  • 创建大量嵌套作用域和闭包函数会带来花销,无论是在内存还是速度上。

其实在大部分应用中,主要的性能瓶颈是在操作DOM节点上。用JavaScript的性能损耗相对DOM操作的性能损耗而言,基本是可以忽略不计的,所以柯里化在大多数场合是可以放心使用。

一道经典面试题

最后,柯里化是实现函数式编程的重要技巧之一,更多深入知识,可以查看本书函数式编程部分的内容。

circle-info

最后更新于

这有帮助吗?