简介
柯里化 (Currying),又称部分求值(Partial Evaluation),是把接收多个参数的函数变成接受一个单一参数(最初函数的第一个参数) 的函数,并且返回接受剩余的参数而且返回结果的新函数的技术。
核心思想 : 把多参数传入的函数拆成单参数(或部分参数) 函数,内部再返回调用下一个单参数 (或部分参数) 函数,依次处理剩余的参数。
按照 Stoyan Stefanov –《JavaScript Pattern》作者 的说法,所谓 柯里化 就是 使函数理解并处理部分应用。
在 JavaScript 中实现 Currying
为了实现 只传递给函数一部分参数来调用它,让它返回一个函数去处理剩余参数 的这句话所描述的特征。我们先实现一个加法函数add
:
function add(x, y) {return x + y}
我们现在实现一个被 Currying 的add
函数,命名该函数为 curriedAdd
,则根据上面的定义,curriedAdd
需要满足以下条件:
curriedAdd(1)(3) === 4 // true
var increment = curriedAdd(1)
increment(2) === 3 // true
var addTen = curriedAdd(10)
addTen(2) === 12 // true
满足以上条件的 curriedAdd
函数可以用以下代码实现:
function curriedAdd(x) {return function(y) {return x + y}
}
当然以上实现有一些问题: 它不通用 ,并且我们并不想通过修改函数被人的方式来实现Currying 化。
但是 curriedAdd
的实现表明了实现 Currying 的一个基础 –Currying延迟求值的特性需要我们用到 JavaScript
中的作用域,说得更通俗一些,我们需要使用作用域 (即闭包) 来保存上一次传进来的参数。
对 curriedAdd
进行抽象,可以得到如下函数currying
:
function currying (fn, ...args1) {return function (...args2) {return fn(...arg1, ...arg2)
}
}
var increment = currying(add, 1)
increment(2) === 3 // true
var addTen = currying(add, 10)
addTen(2) === 12 // true
在此实现中,currying
函数的返回值其实是一个接受剩余参数并且立即返回计算值的函数。即它的返回值并没有自动被 Currying。所以我们可以通过递归将 currying
返回的函数也自动 Currying。
function currying(fn, ...args) {if (args.length >= fn.length) {return fn(...args)
}
return function (...args2) {return currying(fn, ...args, ...args2)
}
}
以上函数很简短,但是已经实现 Currying 的核心思想。JavaScript
中常用库 Lodash
中的 curry
方法,其核心思想和以上并没有太大差异 –比较多次接收的参数总数与函数定义时的形参数量,当接收的参数的数量大于或者等于被 Currying 函数的形参数量时,就返回运行结果,否则返回一个继续接受参数的函数。
Currying 应用场景
参数复用
固定不变的参数,实现参数复用是 Currying 的主要用途之一。
案例一
上文中的 increment
、addTen
的一个参数复用的实例。对 add
方法固定第一个参数为 10 后,该方法就变成了一个将接受累加 10 的方法。
案例二
判断对象的类型。例如下面这个例子:
function isArray (obj) {return Object.prototype.toString.call(obk) === '[object Array]'
}
function isNumber (obj) {return Object.prototype.toString.call(obj) === '[object Number]'
}
function isString (obj) {return Object.prototype.toString.call(obj) === '[object String]'
}
// Test
isArray([1, 2, 3]) // true
isNumber(123) // true
isString('123') // true
但是上面方案有一个问题,那就是每种类型都需要定义一个方法,这里我们可以使用 bind
来扩展,优点是可以直接使用改造后的toStr
:
const toStr = Function.prototype.call.bind(Object.prototype.toString)
// 改造前直接调用
[1, 2, 3].toString() // "1,2,3"
'123'.toString() // "123"
123.toString() // SyntaxError: Invalid or unexpected token
Object(123).toString() // "123"
// 改造后调用 toStr
toStr([1, 2, 3]) // "[object Array]"
toStr('123') // "[object String]"
toStr(123) // "[object Number]"
toStr(Object(123)) // "[object Number]"
上面例子首先使用 Function.prototype.call
函数指定一个 this
值,然后 .bind
返回一个新的函数,始终将 Object.prototype.toString
设置为传入参数,其实等价于 Object.prototype.toString.call()
。
延迟执行
延迟执行也是 Currying 的一个重要使用场景,同样 bind
和箭头函数
也能实现同样的功能。
在前端开发中,一个常见的场景就是为标签绑定 onClick
,同时考虑为绑定的方法传递参数。
以下列出了几种常见的方法,来比较优劣:
通过 data 属性
<div data-name="name" onClick={handleOnClick} />
通过 data
属性本质只能传递 字符串 的数据,如果需要传递复杂对象,只能通过 JSON.stringify(data)
来传递满足 JSON
对象格式的数据,但对更加复杂的对象无法支持。(虽然大多数时候也无需传递复杂对象)
通过 bind 方法
<div onClick={handleOnClick.bind(null, data)} />
bind
方法和以上实现的 currying 方法
,在功能上有极大的相似,在实现上也几乎差不多。可能唯一的不同就是bind
方法需要强制绑定 context
,也就是bind
的第一个参数会作为原函数运行时的 this
指向。而 currying
不需要此参数。所以使用 currying
或者 bind
只是一个取舍问题。
箭头函数
<div onClick={() => handleOnClick(data))} />
箭头函数能够实现延迟执行,同时也不像 bind
方法必需指定context
。
通过 currying
<div onClick={currying(handleOnClick, data)} />
性能对比
通过 jsPerf
测试四种方式的性能,结果为:箭头函数
> bind
> currying
> trueCurrying
。currying
函数相比 bind
函数,其原理相似,但是性能相差巨大,其原因是 bind
由浏览器实现,运行效率有加成。
为什么不需要 Currying
1. Currying 的一些特性有其他解决方案
如果我们只是想提前绑定参数,那么我们有很多好几个现成的选择,bind
,箭头函数
等,而且性能比 Curring
更好。
2. Currying 陷于函数式编程
Currying
是函数式编程的产物,它生于函数式编程,也服务于函数式编程。
而 JavaScript
并非真正的函数式编程语言,相比 Haskell
等函数式编程语言,JavaScript
使用 Currying
等函数式特性有额外的性能开销,也缺乏类型推导。
从而把 JavaScript
代码写得符合函数式编程思想和规范的项目都较少,从而也限制了 Currying
等技术在 JavaScript
代码中的普遍使用。
结论
-
Currying
在JavaScript
中是 低性能 的,但是这些性能在绝大多数场景,是可以忽略的。 -
Currying
的思想极大地助于提升函数的复用性。 -
Currying
生于函数式编程,也陷于函数式编程。假如没有准备好写纯正的函数式代码,那么Currying
有更好的替代品。