初识 JS 中的柯里化

Souleigh ✨ 等级 544 2 1

作为函数式编程语言,JS带来了很多语言上的有趣特性,比如柯里化。

1. 简介

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

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

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

柯里化有3个常见作用:

  1. 参数复用
  2. 提前返回
  3. 延迟计算/运行

talk is cheap,看看怎么实现吧~

2. 实现

2.1 通用实现

一个通用实现:

function currying(fn, ...rest1) {
  return function(...rest2) {
    return fn.apply(null, rest1.concat(rest2))
  }
}

注意这里concat接受非数组元素参数将被当做调用者的一个元素传入

用它将一个sayHello函数柯里化试试:

function sayHello(name, age, fruit) {
  console.log(console.log(`我叫 ${name},我 ${age} 岁了, 我喜欢吃 ${fruit}`))
}

const curryingShowMsg1 = currying(sayHello, '小明')
curryingShowMsg1(22, '苹果')            // 我叫 小明,我 22 岁了, 我喜欢吃 苹果

const curryingShowMsg2 = currying(sayHello, '小衰', 20)
curryingShowMsg2('西瓜')               // 我叫 小衰,我 20 岁了, 我喜欢吃 西瓜

嘻嘻,感觉还行~

2.2 高阶柯里化函数

以上柯里化函数已经能解决一般需求了,但是如果要多层的柯里化总不能不断地进行currying函数的嵌套吧,我们希望经过柯里化之后的函数每次只传递一个或者多个参数,那该怎么做呢:

function curryingHelper(fn, len) {
  const length = len || fn.length  // 第一遍运行length是函数fn一共需要的参数个数,以后是剩余所需要的参数个数
  return function(...rest) {
    return rest.length >= length    // 检查是否传入了fn所需足够的参数
        ? fn.apply(this, rest)
        : curryingHelper(currying.apply(this, [fn].concat(rest)), length - rest.length)        // 在通用currying函数基础上
  }
}

function sayHello(name, age, fruit) { console.log(`我叫 ${name},我 ${age} 岁了, 我喜欢吃 ${fruit}`) }    

const betterShowMsg = curryingHelper(sayHello)
betterShowMsg('小衰', 20, '西瓜')      // 我叫 小衰,我 20 岁了, 我喜欢吃 西瓜
betterShowMsg('小猪')(25, '南瓜')      // 我叫 小猪,我 25 岁了, 我喜欢吃 南瓜
betterShowMsg('小明', 22)('倭瓜')      // 我叫 小明,我 22 岁了, 我喜欢吃 倭瓜
betterShowMsg('小拽')(28)('冬瓜')      // 我叫 小拽,我 28 岁了, 我喜欢吃 冬瓜

如此实现一个高阶的柯里化函数,使得柯里化一个函数的时候可以不用嵌套的currying,当然是因为把嵌套的地方放到了curryingHelper里面进行了...-。-

2.3 疯狂柯里化函数

尽管柯里化函数已经很牛了,但是它也让你必须花费点小心思在你所定义函数的参数顺序上。在一些函数式编程语言中,会定义一个特殊的“占位变量”。通常会指定下划线来干这事,如果作为一个函数的参数被传入,就表明这个是可以“跳过的”,是尚待指定的参数。比如:

var sendAjax = function (url, data, options) { /* ... */ }
var sendPost = function (url, data) {                    // 当然可以这样
    return sendAjax(url, data, { type: "POST", contentType: "application/json" })
}
// 也可以使用下划线来指定未确定的参数
var sendPost = sendAjax( _ , _ , { type: "POST", contentType: "application/json" }) 

JS不具备这样的原生支持,可以使用一个全局占位符变量const _ = { }并且通过===来判断是否是占位符,当然你如果使用了lodash的话可以使用别的符号代替。那么可以这样改造柯里化函数:

const _ = {}
function crazyCurryingHelper(fn, length, args, holes) {
  length = length || fn.length    // 第一遍是fn所需的参数个数,以后是
  args = args || []
  holes = holes || []

  return function(...rest) {
    let _args = args.slice(),
        _holes = holes.slice(),
        argLength = _args.length,        // 存储接收到的args和holes的长度
        holeLength = _holes.length,
        arg, i = 0
    for (; i < rest.length; i++) {
      arg = rest[i]
      if (arg === _ && holeLength) {
        holeLength--                      // 循环_holes的位置
        _holes.push(_holes.shift())      // _holes最后一个移到第一个
      } else if (arg === _) {
        _holes.push(argLength + i)          // 存储_hole就是_的位置
      } else if (holeLength) {              // 是否还有没有填补的hole
        holeLength--
        _args.splice(_holes.shift(), 0, arg)           // 在参数列表指定hole的地方插入当前参数
      } else {
        _args.push(arg)            // 不需要填补hole,直接添加到参数列表里面
      }
    }

    return _args.length >= length                          // 递归的进行柯里化
        ? fn.apply(this, _args)
        : crazyCurryingHelper.call(this, fn, length, _args, _holes)
  }
}

function sayHello(name, age, fruit) { console.log(`我叫 ${name},我 ${age} 岁了, 我喜欢吃 ${fruit}`) }

const betterShowMsg = crazyCurryingHelper(sayHello)
betterShowMsg(_, 20)('小衰', _, '西瓜')          // 我叫 小衰,我 20 岁了, 我喜欢吃 西瓜
betterShowMsg(_, _, '南瓜')('小猪')(25)          // 我叫 小猪,我 25 岁了, 我喜欢吃 南瓜
betterShowMsg('小明')(_, 22)(_, _, '倭瓜')          // 我叫 小明,我 22 岁了, 我喜欢吃 倭瓜
betterShowMsg('小拽')(28)('冬瓜')          // 我叫 小拽,我 28 岁了, 我喜欢吃 冬瓜

牛B闪闪

3. 柯里化的常见用法

3.1 参数复用

通过柯里化方法,缓存参数到闭包内部参数,然后在函数内部将缓存的参数与传入的参数组合后apply/bind/call给函数执行,来实现参数的复用,降低适用范围,提高适用性。

参看以下栗子,官员无论添加后续老婆,都能和合法老婆组合,通过柯里化方法,getWife方法就无需添加多余的合法老婆...

var currying = function(fn) {
  var args = [].slice.call(arguments, 1)      // fn 指官员消化老婆的手段,args 指的是那个合法老婆
  return function(...rest) {
    var newArgs = args.concat(...rest)        // 已经有的老婆和新搞定的老婆们合成一体,方便控制
    return fn.apply(null, newArgs)        // 这些老婆们用 fn 这个手段消化利用,完成韦小宝前辈的壮举并返回
  }
}

var getWife = currying(function() {
  console.log([...arguments].join(';'))          // allwife 就是所有的老婆的,包括暗渡陈仓进来的老婆
}, '合法老婆')

getWife('老婆1', '老婆2', '老婆3')      // 合法老婆;老婆1;老婆2;老婆3
getWife('超越韦小宝的老婆')             // 合法老婆;超越韦小宝的老婆
getWife('超级老婆')                    // 合法老婆;超级老婆

3.2 提高适用性

通用函数解决了兼容性问题,但同时也会再来,使用的不便利性,不同的应用场景往,要传递很多参数,以达到解决特定问题的目的。有时候应用中,同一种规则可能会反复使用,这就可能会造成代码的重复性。

// 未柯里化前
function square(i) { return i * i; }
function dubble(i) { return i * 2; }
function map(handler, list) { return list.map(handler); }

map(square, [1, 2, 3, 4, 5]);        // 数组的每一项平方
map(square, [6, 7, 8, 9, 10]);
map(dubble, [1, 2, 3, 4, 5]);        // 数组的每一项加倍
map(dubble, [6, 7, 8, 9, 10]);

同一规则重复使用,带来代码的重复性,因此可以使用上面的通用柯里化实现改造一下:

// 柯里化后
function square(i) { return i * i; }
function dubble(i) { return i * 2; }
function map(handler, ...list) { return list.map(handler); }

var mapSQ = currying(map, square);
mapSQ([1, 2, 3, 4, 5]);
mapSQ([6, 7, 8, 9, 10]);

var mapDB = currying(map, dubble);
mapDB([1, 2, 3, 4, 5]);
mapDB([6, 7, 8, 9, 10]);

可以看到这里柯里化方法的使用和偏函数比较类似,顺便回顾一下偏函数~

偏函数是创建一个调用另外一个部分(参数或变量已预制的函数)的函数,函数可以根据传入的参数来生成一个真正执行的函数。比如:

const isType = function(type) {
  return function(obj) {
    return Object.prototype.toString.call(obj) === `[object ${type}]`
  }
}
const isString = isType('String')
const isFunction = isType('Function')

这样就用偏函数快速创建了一组判断对象类型的方法~

偏函数固定了函数的某个部分,通过传入的参数或者方法返回一个新的函数来接受剩余的参数,数量可能是一个也可能是多个
柯里化是把一个有n个参数的函数变成n个只有1个参数的函数,例如:add = (x, y, z) => x + y + zcurryAdd = x => y => z => x + y + z
当偏函数接受一个参数并且返回了一个只接受一个参数的函数,与两个接受一个参数的函数curry()()的柯里化函数,这时候两个概念类似。(个人理解不知道对不对)

3.3 延迟执行

柯里化的另一个应用场景是延迟执行。不断的柯里化,累积传入的参数,最后执行。例如累加:

const curryAdd = function(...rest) {
  const _args = rest
  return function cb(...rest) {
    if (rest.length === 0) {
      return _args.reduce((sum, single) => sum += single)
    } else {
      _args.push(...rest)
      return cb
    }
  }
}()                        // 为了保存添加的数,这里要返回一个闭包
curryAdd(1)
curryAdd(2)
curryAdd(3)
curryAdd(4)
curryAdd()               // 最后计算输出:10

更通用的写法,将处理函数提取出来:

const curry = function(fn) {
  const _args = []
  return function cb(...rest) {
    if (rest.length === 0) {
      return fn.apply(this, _args)
    }
    _args.push(...rest)
    return cb
  }
}

const curryAdd = curry((...T) => 
  T.reduce((sum, single) => sum += single)
)
curryAdd(1)
curryAdd(2)
curryAdd(3)
curryAdd(4)
curryAdd()               // 最后计算输出:10

4. Function.prototype.bind 方法也是柯里化应用

call/apply 方法直接执行不同,bind 方法将第一个参数设置为函数执行的上下文,其他参数依次传递给调用方法(函数的主体本身不执行,可以看成是延迟执行),并动态创建返回一个新的函数, 这符合柯里化特点。

var foo = {x: 888};
var bar = function () {
    console.log(this.x);
}.bind(foo);              // 绑定
bar();                    // 888

下面是一个 bind 函数的模拟,testBind 创建并返回新的函数,在新的函数中将真正要执行业务的函数绑定到实参传入的上下文,延迟执行了。

Function.prototype.testBind = function(scope) {
  return () => this.apply(scope)
}
var foo = { x: 888 }
var bar = function() {
  console.log(this.x)
}.testBind(foo)              // 绑定
bar()                    // 888

网上的帖子大多深浅不一,甚至有些前后矛盾,在下的文章都是学习过程中的总结,如果发现错误,欢迎留言指出~

收藏
评论区

相关推荐

初识 JS 中的柯里化
作为函数式编程语言,JS带来了很多语言上的有趣特性,比如柯里化。 1. 简介 柯里化(Currying),又称部分求值(Partial Evaluation),是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。 核心思想是把多参数传入的函数拆成单参数(或部分)函
Next.js页面渲染的优化方案
Next.js页面渲染的优化方案 在过去一年的工作中我所使用的js框架是Next.js,尽管这个框架在前后端同构方面有着绝佳的体验,但是当页面js文件过大以及preload过多的时候还是会出现页面跳转卡顿和渲染阻塞等比较糟糕的用户体验问题。由于我之前既不知道这个框架的工作原理,自然也就不知道如何去优化它。乘着农历春节前工地
uniapp中使用vue-i18n实现国际化多语言
uniapp的项目中 需要用到国际化切换 做一个总结 1. 首先看一下目录结构 2. 准备好vuei18n的js文件(下方有源码地址) 3. la
发现Kotlin一个神奇的bug
1、前言 本文将会通过具体的业务场景,由浅入深的引出Kotlin的一个bug,并告知大家这个bug的神奇之处,接着会带领大家去查找bug出现的原因,最后去规避这个bug。 2、bug复现 现实开发中,我们经常会有将Json字符串反序列化为一个对象问题,这里,我们用Gson来写一段反序列代码,如下: kotlin fun <T fromJson(js
舒文:浅谈阿里前端的多样化
2007年,Jeff Atwood 提出了一个著名的观点, 戏谑又似认真地称其为 Atwood's Law(https://blog.codinghorror.com/theprincipleofleastpower/): _any application that can be written in JavaScript, will event
JavaScript sourceMap 笔记
js source map 建议打开一个真实的项目的sourceMap对照食用由于前端项目在网络中访问导致为了减少体积进行一系列优化操作,最后导致生产环境出问题无法定位到项目代码中的指定位置,使得调试变成一件很难得事。由此产生了Source Map。 它是个什么东西简单说,sourceMap就是一个文件,里面储存着位置信息。仔细点说,这个
js 理解模块化
经常在面试或者其他文章看到关于模块化的问题,之前也只是寥寥看了几次,对于 CommonJS,AMD,ES6也说不出个所以然,于是今天抽空好好看了 红宝书第4版关于模块化的介绍,这里记录一下。 理解模块模式 初衷在开发中肯定有设计大量三方库或者业务逻辑代码,较好的方式是将其分割为多个小模块,最后以一定的方式连接起来
推荐四款可视化工具,解决99%的可视化大屏需求
大家好,我是小五我最经常的工作是将一些项目的数据从数据库导出,然后分门别类的列到excel表格中,领导看起来眼花缭乱。那就开始想了,要是能以图表可视化展现出来,领导就可以看到项目近几个月的走势,也知道之后要怎么决策了。尝试过使用excel制作图表,说实话完全可以实现,,于是在网上找到了以下四种可视化工具,现在我们来看一下:它们的简介和优缺点,如果大家有自己的
只听说过CSS in JS,怎么还有JS in CSS?
CSS in JS是一种解决css问题想法的集合,而不是一个指定的库。从CSS in JS的字面意思可以看出,它是将css样式写在JavaScript文件中,而不需要独立出.css、.less之类的文件。将css放在js中使我们更方便的使用js的变量、模块化、treeshaking。还解决了css中的一些问题,譬如:更方便解决基于状态的样式,更容易追溯依赖关
浅谈JS中的递归
一、递归递归(英语:Recursion)在数学与计算机科学中,是指在函数的定义中使用函数自身的方法在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数其核心思想是把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解一般来说,递归需要有边界条件、递归前进阶段和递归返回阶段。当边界条件不满足时,递归前进;当边界条件满
js二维数组重新转化 Echarts中遇到的数据问题
后台返回的数据 需要重新整合 var arr [[1,1,1],[2,2,2],[3,3,3],[4,4,4],[5,5,5]]var newArray arr[0].map(function(col, i) return arr.map(function(row) return row[i]; ));
Cocos Creator3.x中使用AES加密解密
Cocos Creator升级3x版本之后就不再支持js了,直接装包cryptojs会报错,require 函数在ts里面 根本就不能识别,但是我们项目中需要用到js的包来实现AES加密解密,尝试了多种方法终于修成正果 使用方法import CryptoJS from "cryptojs.min.js";const aseKey "12345678"
项目中的富文本编辑器该如何选择?
项目中经常需要用到富文本编辑器的时候,而常见的富文本编辑器都有哪些?该如何选择?先看看市面上都有哪些可用的富文本编辑器: (插件式的,支持 Vue,React,Angular 框架) (Typescript 开发的 Web 富文本编辑器, 轻量、简洁、易用、开源免费,支持 JS 直接引入使用,或者 Vue2/3,React) (开源,插件多,功能齐全,支持
javascript实践教程-02-javascript入门
本节目标1. 掌握如何编写javascript代码。2. 掌握javascript的3个弹框。3. 掌握javascript的注释。4. 掌握浏览器的调试工具控制台。 内容摘要本篇介绍了如何在网页上编写js代码,如何引入外部js代码文件,js的3个弹框、注释语法,还有浏览器调试工具的控制台使用。阅读时间1520分钟。 script标签如果我们需要在网页中编写
javascript实践教程-07-分支结构
本节目标1. 掌握js中4种分支结构。 内容摘要本篇介绍了js中的4种分支结构:if、if else、else if、switch case,用来判断在不同的条件下运行不同的代码分支。阅读时间1015分钟。 分支结构js中分支结构总共有4种: if if else else if switch case ifif 用来判断某个条件是否成立,如果成立则执行条