乐趣区

JavaScript之柯里化

简介

柯里化 (Currying),又称部分求值(Partial Evaluation),是把接收多个参数的函数变成接受一个单一参数(最初函数的第一个参数) 的函数,并且返回接受剩余的参数而且返回结果的新函数的技术。

核心思想 : 把多参数传入的函数拆成单参数(或部分参数) 函数,内部再返回调用下一个单参数 (或部分参数) 函数,依次处理剩余的参数。

按照 Stoyan Stefanov –《JavaScript Pattern》作者 的说法,所谓 柯里化 就是 使函数理解并处理部分应用

在 JavaScript 中实现 Currying

为了实现 只传递给函数一部分参数来调用它,让它返回一个函数去处理剩余参数 的这句话所描述的特征。我们先实现一个加法函数add:

function add(x, y) {return x + y}

我们现在实现一个被 Curryingadd函数,命名该函数为 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 的主要用途之一。

案例一

上文中的 incrementaddTen 的一个参数复用的实例。对 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 代码中的普遍使用。

结论

  1. CurryingJavaScript 中是 低性能 的,但是这些性能在绝大多数场景,是可以忽略的。
  2. Currying的思想极大地助于提升函数的复用性。
  3. Currying 生于函数式编程,也陷于函数式编程。假如没有准备好写纯正的函数式代码,那么 Currying 有更好的替代品。
退出移动版