柯里化(Currying),又称部分求值(Partial Evaluation),是把接收多个参数的函数变成接受一个单一参数(最初函数的第一个参数)的函数,而且返回接受剩余的参数并且返回结果的新函数的技术。
核心思想: 把多参数传入的函数拆成单参数(或部分参数)函数,内部再返回调用下一个单参数(或部分参数)函数,依次处理剩余的参数。javascript
按照Stoyan Stefanov --《JavaScript Pattern》做者 的说法,所谓柯里化就是使函数理解并处理部分应用。html
为了实现只传递给函数一部分参数来调用它,让它返回一个函数去处理剩余参数的这句话所描述的特征。咱们先实现一个加法函数add
:前端
function add(x, y) { return x + y }
咱们如今实现一个被Currying的add
函数,命名该函数为curriedAdd
,则根据上面的定义,curriedAdd
须要知足如下条件:vue
curriedAdd(1)(3) === 4 // true var increment = curriedAdd(1) increment(2) === 3 // true var addTen = curriedAdd(10) addTen(2) === 12 // true
知足以上条件的curriedAdd
函数能够用如下代码实现:java
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的主要用途之一。
上文中的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
,同时考虑为绑定的方法传递参数。
如下列出了几种常见的方法,来比较优劣:
<div data-name="name" onClick={handleOnClick} />
经过data
属性本质只能传递字符串的数据,若是须要传递复杂对象,只能经过 JSON.stringify(data)
来传递知足JSON
对象格式的数据,但对更加复杂的对象没法支持。(虽然大多数时候也无需传递复杂对象)
<div onClick={handleOnClick.bind(null, data)} />
bind
方法和以上实现的currying 方法
,在功能上有极大的类似,在实现上也几乎差很少。可能惟一的不一样就是bind
方法须要强制绑定context
,也就是bind
的第一个参数会做为原函数运行时的this
指向。而currying
不须要此参数。因此使用currying
或者bind
只是一个取舍问题。
<div onClick={() => handleOnClick(data))} />
箭头函数可以实现延迟执行,同时也不像bind
方法必需指定context
。
<div onClick={currying(handleOnClick, data)} />
经过jsPerf
测试四种方式的性能,结果为:箭头函数
> bind
> currying
> trueCurrying
。currying
函数相比bind
函数,其原理类似,可是性能相差巨大,其缘由是bind
由浏览器实现,运行效率有加成。
若是咱们只是想提早绑定参数,那么咱们有不少好几个现成的选择,bind
,箭头函数
等,并且性能比Curring
更好。
Currying
是函数式编程的产物,它生于函数式编程,也服务于函数式编程。
而JavaScript
并不是真正的函数式编程语言,相比Haskell
等函数式编程语言,JavaScript
使用Currying
等函数式特性有额外的性能开销,也缺少类型推导。
从而把JavaScript
代码写得符合函数式编程思想和规范的项目都较少,从而也限制了 Currying
等技术在JavaScript
代码中的广泛使用。
Currying
在JavaScript
中是低性能的,可是这些性能在绝大多数场景,是能够忽略的。Currying
的思想极大地助于提高函数的复用性。Currying
生于函数式编程,也陷于函数式编程。假如没有准备好写纯正的函数式代码,那么Currying
有更好的替代品。个人博客即将同步至腾讯云+社区,邀请你们一同入驻:https://cloud.tencent.com/dev...