JavaScript Infinite Currying
很久之前曾看到一个很有意思的 JS 问题,
// 定义一个函数 add,满足如下性质:
add(1) == 1
add(1)(2) == 3
add(1)(2)(3) == 6
...
var g = add(1)(2)
g(100) == 103
g(200) == 203
...
乍一看,这应该是需要用到柯里化(Curry)的知识,但是好像又不够。当时忙别的事情就没管了,现在想起来,便认真研究了一下。
首先我们需要来说一下柯里化,柯里化是一个在函数编程中十分重要的概念,如果大家熟悉 Haskell
的话就知道 Haskell
中的函数都是默认柯里化的。JS 随便找一个函数编程库(比如 Ramda)肯定也会有柯里化,因为他实在是太重要了。这里我们用一个简单的例子来看看什么是柯里化。
// f 是一个普通函数,接受两个参数,并返回他们相加的结果
function f(x, y) {
return x + y
}
// g 是一个柯里化函数,接受一个参数,返回一个新的函数
function g(x) {
return function (y) {
return x + y
}
}
// 传统函数调用是接收多个参数返回一个值,而柯里化函数则是接收参数返回新的函数,新的函数又可以接受参数再返回新的函数,直至最后返回结果值
// 使用柯里化函数的优势是我们可以"部分应用" (Partial Application)函数的参数,生成新的函数,这在函数编程中是至关重要的
// g(1)(2) == 3
// var add1 = g(1)
// add1(100) == 101
// var add100 = g(100)
// add100(100) == 200
现在我们来分析上面的问题。
首先,add(1) == 1
,说明 add
函数应该返回一个整数。但是 add(1)(2) == 3
表明毫无疑问 add(1)
返回的值应该是一个函数。所以现在我们的问题就变成了,有没有可能让一个函数等于一个整数呢?(注意比较操作符是松散的==
,而不是严格===
)。
答案是有可能的。这里需要我们了解 JS 的一个小知识。那就是 valueOf
属性。当我们将一个对象和一个 Primitive 进行比较的时候,JS 会调用对象的 valueOf
方法获取一个 Primitive 值,然后再进行比较。
var a = {}
a.valueOf = function () {
return 'hello world!'
}
a == 'hello world!' // true
a === 'hello world!' // false,严格等于操作符会比较数据类型
从这里就可以看出,只要我们定义了对象 a 的 valueOf
方法,我们可以让他和任意的 Primitive 值相等。
函数也是一个对象,所以这个问题的解决方法就很清楚了。每次返回的都是一个函数。这个函数的 valueOf
会返回传入参数的和。
大家可以试试看自己实现,下面是我的实现~
function total(args) {
return [].slice.call(args).reduce((t, c) => t + c, 0)
}
function add() {
function factor(value) {
var result = function () {
return factor(value + total(arguments))
}
result.value = value
result.valueOf = function () {
return this.value
}
return result
}
return factor(total(arguments))
}