JavaScript之柯里化
简介
柯里化(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
有更好的替代品。
我的博客即将同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/dev...