首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >柯里化与反柯里化

柯里化与反柯里化

作者头像
贾顺名
发布2018-04-12 14:21:32
9830
发布2018-04-12 14:21:32
举报
文章被收录于专栏:全沾开发(huā)全沾开发(huā)

柯里化与反柯里化

最近在看一本书《JavaScript函数式编程》 里边提到了一个名词,柯里化(currying),阅读后发现在日常开发中经常会用到柯里化函数。 以及还有他的反义词反柯里化(unCurrying) 柯里化被称为部分计算函数,也就是会固定一部分参数,然后返回一个接收剩余参数的函数。目的是为了缩小适用范围,创建一个针对性更强的函数。 反柯里化正好与之相反,我们是要扩大一个函数的适用范围,比如将Array独有的push应用到一个Object上去。

两种方案的通用代码实现

function currying (func, ...preArgs) {
  let self = this
  return function (...args) {
    return func.apply(self, [].concat(preArgs, args))
  }
}

function unCurrying (func) {
  return function (reference, ...args) {
    return func.apply(reference, args)
  }
}

两种方案的简单示意

currying

foo(arg1, arg2)
// =>
foo(arg1)(arg2)

unCurrying

obj.foo(arg1, arg2)
// =>
foo(obj, arg1, arg2)

柯里化currying

一个柯里化函数的简单应用,我们有一个进行三个参数求和的函数。 我们可以调用currying传入sum获得sum1,一个固定了第一个参数为10的求和函数 然后我们又调用currying传入sum1获得sum2,在原有的固定了一个参数的基础上,再次固定一个参数20

这时我们调用sum2时仅需传入一个参数即可完成三个参数的求和:10 + 20 + n

let sum = (a, b, c) => a + b + c // 一个进行三个参数求和的函数
let sum1 = currying(sum, 10)     // 固定第一个参数

console.log(sum1(1, 1))          // 12
console.log(sum1(2, 2))          // 14

let sum2 = currying(sum1, 20)    // 固定第二个参数

console.log(sum2(1))             // 31
console.log(sum2(2))             // 32

帮助人理解currying最简单的例子就是XXX.bind(this, yourArgs)() 写过React的人应该都知道,在一些列表需要绑定事件时,我们大致会有这样的代码:

{
  // ...
  clickHandler (id) {
    console.log(`trigger with: ${id}`)
  },
  render () {
    return (<ul>
      {this.state.data.map(item =>
        <li onClick={this.clickHandler.bind(this, item.id)}>{item.name}</li>
      )}
    </li>)
  }
}

这样我们就能在点击事件被触发时拿到对应的ID了。这其实就是一个函数柯里化的操作 我们通过bind生成了多个函数,每个函数都固定了第一个参数index,然后第二个参数才是event对象。

又或者我们有如下结构的数据,我们需要新增一列数据的展示description,要求格式为所在部门-姓名

const data = [{
  section: 'S1',
  personnel: [{
    name: 'Niko'
  }, {
    name: 'Bellic'
  }]
}, {
  section: 'S2',
  personnel: [{
    name: 'Roman'
  }]
}]

如果用普通函数的处理方法,可能是这样的:

let result = data.map(sections => {
  sections.personnel = sections.personnel.map(people => {
    people.description = `${sections.section}-${people.name}`

    return people
  })

  return sections
})

或者我们可以用currying的方式来实现

let result = data.map(sections => {
  sections.personnel = sections.personnel.map(currying((section, people) => {
    people.description = `${section}-${people.name}`

    return people
  }, sections.section))

  return sections
})

使用柯里化还有一种好处,就是可以帮助我们明确调用函数的参数。 我们创建一个如下函数,一个看似非常鸡肋的函数,大致作用如下:

  • 接收一个函数
  • 返回一个只接收一个参数的函数
function curry (func) {
  return function (arg) {
    return func(arg)
  }
}

我们应该都用过一个全局函数parseInt 用来将String转换为Number parseInt('10') // 10 但其实,parseInt不止接收一个参数。 parseInt('10', 2) // 2 第二个参数可以用来标识给定值的基数,告诉我们用N进制来处理这个字符串

所以当我们直接将一个parseInt传入map中时就会遇到一些问题:

['1', '2', '3', '4'].map(parseInt) // => 1, NaN, NaN, NaN

因为map回调的返回值有三个参数当前item当前item对应的index调用map的对象引用 所以我们可以用上边的curry函数来解决这个问题,限制parseInt只接收一个参数

['1', '2', '3', '4'].map(curry(parseInt)) // => 1, 2, 3, 4

缩小适用范围,创建一个针对性更强的函数

反柯里化unCurrying

虽说名字叫反柯里化。。但是我觉得也只是部分理念上相反,而不是向Math.maxMath.min,又或者[].pop[].push这样的完全相反。 就像柯里化是缩小了适用范围,所以反柯里化所做的就是扩大适用范围。

这个在开发中也会经常用到,比如某宝有一个经典的面试题: 如何获取一个页面中所用到的所有标签,并将其输出?

// 普通函数的写法
let tags = []
document.querySelectorAll('*').forEach(item => tags.push(item.tagName))

tags = [...new Set(tags)] // => [a, b, div, ...]

因为qsa返回的是一个NodeList对象,一个类数组的对象,他是没有直接实现map方法的。 而反柯里化就是用来帮助它实现这个的,扩大适用范围,让一些原本无法调用的函数变得可用

let map = unCurrying([].map)
let tags = map(document.querySelectorAll('*'), item => item.tagName)

tags = [...new Set(tags)] // 其实可以合并到上边那一行代码去,但是这样看起来更清晰一些

又或者早期写JavaScript时对arguments对象的处理,这也是一个类数组对象。 比如一些早期版本的currying函数实现(手动斜眼):

function old_currying () {
  let self = this
  let func = arguments[0]
  let preArgs = [].slice.call(arguments, 1)
  return function () {
    func.call(self, [].concat(preArgs, arguments))
  }
}

里边用到的[].slice.call经过一层封装后,其实就是实现的unCurrying的效果

网上流传的一个有趣的面试题

有大概这么一道题,如何实现下面的函数:

var a = func(1)

console.log(+a)       // => 1
console.log(+a(2))    // => 3
console.log(+a(2)(3)) // 6

这里是一个实现的方案:https://github.com/Jiasm/notebook/blob/master/currying.js

一个柯里化实现的变体。

小记

在《JavaScript函数式编程》中提到了,高阶函数的几个特性:

  1. 以一个函数作为参数
  2. 以一个函数作为返回值

柯里化/反柯里化只是其中的一小部分。 其实柯里化还分为了向右柯里化向左柯里化(大概就是preArgsargs的调用顺序问题了)

用函数构建出新的函数,将函数组合在一起,这个是贯穿这本书的一个理念,在现在大量的面向对象编程开发中,能够看到这么一本书,感觉很是清新。从另一个角度看待JavaScript这门语言,强烈推荐,值得一看

文章部分示例代码:https://github.com/Jiasm/currying-uncurrying

参考资料

http://2ality.com/2011/11/uncurrying-this.html

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 柯里化与反柯里化
  • 两种方案的通用代码实现
  • 两种方案的简单示意
    • currying
      • unCurrying
      • 柯里化currying
      • 反柯里化unCurrying
      • 网上流传的一个有趣的面试题
      • 小记
      • 参考资料
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档