JavaScript 柯里化

简介

柯里化从何而来

柯里化, 即 Currying 的音译。 Currying 是编译原理层面实现多参函数的一个技术。

在说JavaScript 中的柯里化前,可以聊一下原始的Currying是什么,又从何而来。

在编码过程中,身为码农的我们本质上所进行的工作就是——将复杂问题分解为多个可编程的小问题。

Currying 为实现多参函数提供了一个递归降解的实现思路——把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数,在某些编程语言中(如 Haskell),是通过 Currying 技术支持多参函数这一语言特性的。

所以 Currying 原本是一门编译原理层面的技术,用途是实现多参函数

柯里化去向哪里

在 Haskell 中,函数作为一等公民,Currying 从编译原理层面的技术应运而成了一个语言特性。 在语言特性层面,Currying 是什么?

在《Mostly adequate guide》一书中,这样总结了 Currying ——只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数

所以 Currying 是应函数式编程而生,在有了 Currying 后,大家再去探索去发掘了它的用途及意义。 然后因为这些用途和意义,大家才积极地将它扩展到其他编程语言中。

在 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(...args1, ...args2)

    }
}

var increment = currying(add, 1)

increment(2) === 3

// true

var addTen = currying(add, 10)

addTen(2) === 12

// true

在此实现中,currying 函数的返回值其实是一个接收剩余参数并且立即返回计算值的函数。即它的返回值并没有自动被 Currying化 。所以我们可以通过递归来将 currying 的返回的函数也自动 Currying 化。

function trueCurrying(fn, ...args) {

    if (args.length >= fn.length) {

        return fn(...args)

    }

    return function (...args2) {

        return trueCurrying(fn, ...args, ...args2)

    }
}

以上函数很简短,但是已经实现 Currying 的核心思想了。JavaScript 中的常用库 Lodash 中的 curry 方法,其核心思想和以上并没有太大差异——比较多次接受的参数总数与函数定义时的入参数量,当接受参数的数量大于或等于被 Currying 函数的传入参数数量时,就返回计算结果,否则返回一个继续接受参数的函数。

Lodash 中实现 Currying 的代码段较长,因为它考虑了更多的事情,比如绑定 this 变量等。在此处就不直接贴出 Lodash 中的代码段,感兴趣的同学可以去看看看 Lodash 源码,比较一下这两种实现会导致什么样的差异。

然而 Currying 的定义和实现都不是最重要的,本文想要阐述的重点是:它能够解决编码和开发当中怎样的问题,以及在面对不同的问题时,选择一个合适的 Currying,来最恰当的解决问题

Currying 使用场景

参数复用

固定不变的参数,实现参数复用是 Currying 的主要用途之一。

上文中的increment, addTen是一个参数复用的实例。对add方法固定第一个参数为 10 后,改方法就变成了一个将接受的变量值加 10 的方法。

延迟执行

延迟执行也是 Currying 的一个重要使用场景,同样 bind 和箭头函数也能实现同样的功能。

在前端开发中,一个常见的场景就是为标签绑定 onClick 事件,同时考虑为绑定的方法传递参数。

以下列出了几种常见的方法,来比较优劣:

  1. 通过 data 属性

    通过 data 属性本质只能传递字符串的数据,如果需要传递复杂对象,只能通过 JSON.stringify(data) 来传递满足 JSON 对象格式的数据,但对更加复杂的对象无法支持。(虽然大多数时候也无需传递复杂对象)

  2. 通过bind方法

    bind 方法和以上实现的 currying 方法,在功能上有极大的相似,在实现上也几乎差不多。可能唯一的不同就是 bind 方法需要强制绑定 context,也就是 bind 的第一个参数会作为原函数运行时的 this 指向。而 currying 不需要此参数。所以使用 currying 或者 bind 只是一个取舍问题。

  3. 箭头函数

    handleOnClick(data))} />

    箭头函数能够实现延迟执行,同时也不像 bind 方法必需指定 context。可能唯一需要顾虑的就是在 react 中,会有人反对在 jsx 标签内写箭头函数,这样子容易导致直接在 jsx 标签内写业务逻辑。

  4. 通过currying

性能对比

JavaScript 柯里化_第1张图片
image.png

通过 jsPerf 测试四种方式的性能,结果为:箭头函数>bind>currying>trueCurrying

currying 函数相比 bind 函数,其原理相似,但是性能相差巨大,其原因是 bind 由浏览器实现,运行效率有加成。

从这个结果看 Currying 性能无疑是最差的,但是另一方面就算最差的 trueCurrying 的实现,也能在本人的个人电脑上达到 50w Ops/s 的情况下,说明这些性能是无需在意的。

trueCurrying 方法中实现的自动 Currying 化,是另外三个方法所不具备的。

到底需不需要 Currying

为什么需要 Currying

  1. 为了多参函数复用性

    Currying 让人眼前一亮的地方在于,让人觉得函数还能这样子复用。

    通过一行代码,将 add 函数转换为 increment,addTen 等。

    对于 Currying 的复杂实现中,以 Lodash 为列,提供了 placeholder 的神奇操作。对多参函数的复用玩出花样。

    import _ from 'loadsh'
    
    function abc (a, b, c) {
      return [a, b, c];
    }
    
    var curried = _.curry(abc)
    
    // Curried with placeholders.
    curried(1)(_, 3)(2)
    // => [1, 2, 3]
    
    
  2. 为函数式编程而生

    Currying 是为函数式而生的东西。应运着有一整套函数式编程的东西,纯函数composecontainer等等事物。(可阅读《mostly-adequate-guide》 )

    假如要写 Pointfree Javascript 风格的代码,那么Currying是不可或缺的。

    要使用 compose,要使用 container 等事物,我们也需要 Currying。

为什么不需要 Currying

  1. Currying 的一些特性有其他解决方案

    如果我们只是想提前绑定参数,那么我们有很多好几个现成的选择,bind,箭头函数等,而且性能比Curring更好。

  2. Currying 陷于函数式编程

    在本文中,提供了一个 trueCurrying 的实现,这个实现也是最符合 Currying 定义的,也提供 了bind,箭头函数等不具备的“新奇”特性——可持续的 Currying(这个词是本人临时造的)。

    但是这个“新奇”特性的应用并非想象得那么广泛。

    其原因在于,Currying 是函数式编程的产物,它生于函数式编程,也服务于函数式编程。

    而 JavaScript 并非真正的函数式编程语言,相比 Haskell 等函数式编程语言,JavaScript 使用 Currying 等函数式特性有额外的性能开销,也缺乏类型推导。

    从而把 JavaScript 代码写得符合函数式编程思想和规范的项目都较少,从而也限制了 Currying 等技术在 JavaScript 代码中的普遍使用。

    假如我们还没有准备好去写函数式编程规范的代码,仅需要在 JSX 代码中提前绑定一次参数,那么 bind 或箭头函数就足够了。

结论

  1. Currying 在 JavaScript 中是“低性能”的,但是这些性能在绝大多数场景,是可以忽略的。
  2. Currying 的思想极大地助于提升函数的复用性。
  3. Currying 生于函数式编程,也陷于函数式编程。假如没有准备好写纯正的函数式代码,那么 Currying 有更好的替代品。
  4. 函数式编程及其思想,是值得关注、学习和应用的事物。所以在文末再次安利 JavaScript 程序员阅读此书 —— 《mostly-adequate-guide》

参考链接

  • 柯里化——维基百科
  • 《JS 函数式编程指南》
  • Pointfree 编程风格指南——阮一峰

https://juejin.im/post/5af13664f265da0ba266efcf?utm_source=gold_browser_extension

你可能感兴趣的:(JavaScript 柯里化)