JavaScript函数柯里化

86 阅读3分钟

本篇将介绍什么是函数柯里化,以及柯里化如何应用。最终我们还会实现一个柯里化函数(Currying)。

什么是函数柯里化?

维基百科是这么定义的:

In mathematics and computer science, currying is the technique of translating the evaluation of a function that takes multiple arguments into evaluating a sequence of functions, each with a single argument.

意为在数学和计算机科学中,柯里化是一种将使用多个参数的函数转换成为一系列使用一个参数的函数的技术。换句话说就是把一个有n个参数的函数转换成n个嵌套的函数,每个函数只接受一个参数,并返回一个新函数。也就是把f(a, b, c)转化为f(a)(b)(c)

柯里化如何应用?

在了解什么是柯里化后,如何在开发中应用,又能解决什么问题呢?

我们来看第一个例子:

// 假设我们有一个封装的 request 函数用于发起 ajax 请求
const request = (method, url, data) => {
    // ...具体实现逻辑
}

// 虽然我们已经对 request 进行了封装,但是在重复调用时还是明显感受到了参数的冗余
request('post', 'path/aaa', { name: '远山' })
request('post', 'path/bbb', { name: '远山1937' })
request('post', 'path/ccc', { name: 'candysoulmate' })

request('get', 'path/dd', { name: '远山' })
request('get', 'path/ee', { name: '远山' })
request('get', 'path/ff', { name: '远山' })

// 利用 curry 函数处理
const reqCurry = curry(request)

// 生成一个用来发起 post 请求的函数
const postReq = reqCurry('post')

// 生成一个用来发起 get 请求的函数
const getReq = reqCurry('get')

// 再发起 post 请求时我们精简了入参,达到了参数复用的效果
postReq('path/aaa', { name: '远山' })

// 发起 get 请求
getReq('path/dd', { name: '远山' })

在这个例子中,我们封装的request是一个通用的工具函数,在重复调用时还是明显感受到了参数的冗余。在使用 curry 函数处理后,我们通过传入不同的method参数生成了postReqgetReq函数。这两个函数与request相比,通用性降低了,但是适用性增加了。在这里例子中我们使用柯里化来做到了参数复用

我们再来看第二个例子:

// 假定有这样一个数组
const list = [
    {
        name:'lucy'
    },
    {
        name:'jack'
    }
]

// 如果想要获取数组中所有的 name ,我们通常会这么写
const names = list.map(i => i.name)

如果用柯里化的思维来实现,则是这样的:

const prop = curry(function (key, object) {
    return object[key]
})

const names = list.map(prop('name'))

在第二个例子中,我们姑且不去讨论哪一种写法更简洁,更受欢迎。理解函数柯里化,我们可以从数学的角度来看。结合两个例子,柯里化的思维是一个逐次消元的过程,当把函数的元全消掉,就得到了值。

// 有如下二元函数
f(x, y) = x + y

// 在y=1时,带入得
g(x) = f(x, 1) = x + 1

实现一个 curry 函数

我们来梳理一下curry函数的特点:

  1. 调用curry函数后会返回一个新的函数
  2. 调用这个新返回的函数,会进行参数的收集,并进行判断:
    • 收集到的参数个数满足调用 fn 需要的参数个数时,调用 fn 并传入收集到的参数
    • 反之,收集到的参数个数不满足 fn 需要的参数个数时,返回新的函数继续收集参数
function curry(fn) {
    return _curry.call(this, fn, fn.length)
}

function _curry(fn, len, ...args) {
    return function(...params) {
        const _args = [...args, ...params]
        if (_args.length >= len) {
            // 收集到的参数个数满足调用 fn 需要的参数个数时
       	    return fn.apply(this, _args)
        } else {
            // 返回新的函数继续收集参数
            return _curry.call(this, fn, len, ..._args)
        }
    }
}

我们来验证一下该函数:

const sumCurry = curry(function(a,b,c){
    return a + b + c
})

sumCurry(1)(2)(3) // 6
sumCurry(1, 2, 3) // 6
sumCurry(1)(2, 3) // 6
sumCurry(1, 2)(3) // 6

满足我们的预期。