上一篇介绍了闭包和高阶函数,这是函数式编程的基础核心。这一篇来看看高阶函数的实战场景。

首先强调两点:

  • 注意闭包的生成位置,清楚作用域链,知道闭包生成后缓存了哪些变量
  • 高阶函数思想:以变量作用域作为根基,以闭包为工具来实现各种功能

柯里化(curry)

定义:柯里化是把一个多参数函数转换为一个嵌套的一元函数的过程

先看个简单的例子,这是一个名为 add 的函数:const add = (x, y) => x + y;调用该函数 add(1, 1)、add(1, 2)、add(1, 3)...很普通,缺乏灵活性。

下面是柯里化实现版本:

const addCurried = x => y => x + y;

如果我们用一个单一的参数调用 addCurried,const add1 = addCurried(1)它返回一个函数fn = y => 1 + y,在其中 x 值通过闭包缓存下来。接下来,我们继续传参add1(1); add1(2); add1(3),有没有感觉比上面的 add 灵活。

上面的实现只是针对接收两个参数相加的柯里化函数,接下来正是开始实现个基础的通用的接收两个参数的柯里化函数:

const curry = (binaryFn) => {
return function (firstArg) {
return function (secondArg) {
return binaryFn (firstArg, secondArg) ; // 为啥要嵌套那么多呢?基于什么思路呢?思考一下...
};
};
};

现在可以用如下方式通过 curry 函数把 add 函数转换成一个柯里化版本:

const autoCurriedAdd = curry(add)
autoCurriedAdd(1)(1) // 2

这里我们已经体会到柯里化的好处了,那么柯里化是怎样实现的呢?看上面 curry 的实现很容易发现,先传入一个接受二元函数,然后返回一个一元函数,当这个一元函数执行后,再返回一个一元函数,再次执行返回的一元函数时,触发最开始那个二元函数的执行。

这里有一个点很重要——执行时机,接收够两个参数(add 函数接收的参数数量)立即执行,也就是说接收够被柯里化函数的参数数量时触发执行

好的,我们已经实现了一个基础的柯里化函数。不过,这个 柯里化函数有很大的局限性——只能用于接收两个参数的函数。我们需要的是被柯里化函数的参数可以任意数量,怎么办呢?还好我们已经知道了被柯里化函数的执行时机——接收够被柯里化函数的参数数量时触发执行。下面我们来实现更复杂的柯里化:

// 柯里化函数
const curry = (fn) => {
if (typeof fn !== 'function') {
throw Error('No function provided')
} return function curriedFn (...args) {
if (fn.length > args.length) { // 未达到触发条件,继续收集参数
return function () {
return curriedFn.apply(null, args.concat([].slice.call(arguments)))
}
}
return fn.apply(null, args)
}
}

这样,我们就能处理多个参数的函数了。比如:

const multiply = (x, y, z) => x*y*z;

const curryMul = curry(multiply);
const result = curryMul(1)(2)(3); // 1*2*3 = 6

偏应用(partial)

偏应用,又称作部分应用,它允许开发者部分地应用函数参数。实际上,偏应用是为一个多元函数预先提供部分参数,从而在调用时可以省略这些参数

比如我们要在每10ms做一组操作。可以通过 setTimeout 函数以如下方式实现:

setTimeout( () => console.log("Do X task"), 10);
setTimeout( () => console.log("Do Y tash"), 10);

很显然,我们可以用上面的 curry 函数包装成柯里化函数,实现灵活调用:

// 实现一个二元函数,用于柯里化
const setTimeoutWrapper = (time, fn) => {
setTimeout(fn, time);
} // 使用 curry 函数封装 setTimeout 来实现一个10ms延迟
const delayTenMs = curry(setTimeoutWrapper)
delayTenMs( () => console.log("Do X task") );
delayTenMs( () => console.log("Do Y task") );

很棒,也能实现灵活调用。但问题是我们不得不创建 setTimeoutWrapper 一样的封装器,这也是一种开销。下面我们看看偏应用的实现:

// 偏应用函数
const partial = (fn, ...partialArgs) => {
let args = partialArgs
return (...fullArguments) => {
let count = 0
for (let i = 0; i < args.length && count < fullArguments; i++) {
if (args[i] === undefined) {
args[i] = fullArguments[count++]
}
}
return fn.apply(null, args)
}
}

下面用偏应用解决上面的延时10ms问题:

let delayTenMs = partial(setTimeout, undefined, 10);  // 注意此处,让我们少创建了一个 setTimeoutWrapper 封装器
delayTenMs( () => console.log("Do X task") )
delayTenMs( () => console.log("Do Y task") );

现在我们对柯里化有了更清晰的认识。创建偏应用函数时,第一个参数接收一个函数,剩余参数是第一个传入函数所需参数。剩余参数待传入的用undefined占位,执行偏应用函数时填充undefined

组合(compose)

在了解什么是函数式组合之前,让我们理解组合的概念。

符合“|”被称为管道,它允许我们通过组合一些函数去创建一个能够解决问题的新函数。大致来说,“|”将最左侧的函数输出作为输入发送给最右侧的函数!从技术上讲,该处理过程称为“管道”。

compose 函数:

const compose = (a, b) => (c) => a(b(c))

compose 函数会首先执行 b 函数,并将 b 的返回值作为参数传递给 a。该函数调用的方向是从右至左的(先执行 b,再执行 a)。

可以看到,组合函数 compose 就是传入一些函数。对于传入的函数,我们要求一个函数只做一件事

下面看下如何应用 compose 函数:

// 通过组合计算字符串单词个数
let splitIntoSpaces = (str) => str.split(" "); // 分割成数组
let count = (array) => array.length; // 计算长度 const countWords = compose(count, splitIntoSpaces); countWord("hello your reading about composition"); // 5

上面的 compose 只能实现两个函数的组合。如何组合更多个函数呢?这就需要借助reduce的威力了:

// 组合多个函数 composeN
const composeN = (...fns) =>
(value) =>
fns.reverse().reduce((acc, fn) => fn(acc), value);

管道/序列(pipe)

管道和组合的概念很类似,都是串行处理数据。唯一区别就是执行方向:组合从右向左执行,管道从左向右执行。

// 组合多个函数 pipe
const pipe= (...fns) =>
(value) =>
fns.reduce((acc, fn) => fn(acc), value);

下面看下如何应用 pipe 函数:

// 通过管道计算字符串单词个数
let splitIntoSpaces = (str) => str.split(" "); // 分割成数组
let count = (array) => array.length; // 计算长度 const countWords = pipe(splitIntoSpaces, count); // 注意此处的传参顺序 countWord("hello your reading about composition"); // 5

总结

通过这一节的学习,我们知道了高阶函数的一些应用——柯里化、偏应用、组合和管道,每种应用都有特定的应用场景。

其中,柯里化是最常用的一种场景,它的作用是把一个多参数函数转换为一个嵌套的一元函数的过程。随着闭包的产生,我们可以灵活的调用。

组合和管道类似,都是串行处理数据。传入一个初始数据,通过一系列特定顺序的纯函数处理成我们希望得到的数据。

参考链接:

简明 JavaScript 函数式编程——入门篇

JavaScript ES6函数式编程(二):柯里化、偏应用和组合、管道的更多相关文章

  1. js函数式编程(二)-柯里化

    这节开始讲的例子都使用简单的TS来写,尽量做到和es6差别不大,正文如下 我们在编程中必然需要用到一些变量存储数据,供今后其他地方调用.而函数式编程有一个要领就是最好不要依赖外部变量(当然允许通过参数 ...

  2. 函数式编程之柯里化(curry)

    函数式编程curry的概念: 只传递给函数一部分参数来调用函数,然后返回一个函数去处理剩下的参数. var add = function(x) { return function(y) { retur ...

  3. JavaScript ES6函数式编程(三):函子

    前面二篇学习了函数式编程的基本概念和常见用法.今天,我们来学习函数式编程的最后一个概念--函子(Functor). 相信有一部分同学对这个概念很陌生,毕竟现在已经有很多成熟的轮子,基本能满足我们日常的 ...

  4. 浅析 JavaScript 中的 函数 currying 柯里化

    原文:浅析 JavaScript 中的 函数 currying 柯里化 何为Curry化/柯里化? curry化来源与数学家 Haskell Curry的名字 (编程语言 Haskell也是以他的名字 ...

  5. JavaScript ES6函数式编程(一):闭包与高阶函数

    函数式编程的历史 函数的第一原则是要小,第二原则则是要更小 -- ROBERT C. MARTIN 解释一下上面那句话,就是我们常说的一个函数只做一件事,比如:将字符串首字母和尾字母都改成大写,我们此 ...

  6. 《JavaScript ES6 函数式编程入门经典》--推荐指数⭐⭐⭐

    这本书比较基础认真看完再自己写点demo一个双休日就差不多, 总体来说看完还是有收获的,会激起一些你对函数编程的兴趣 主要目录如下: 第1章 函数式编程简介 11.1 什么是函数式编程?为何它重要 1 ...

  7. JavaScript 闭包&基于闭包实现柯里化和bind

    闭包: 1 函数内声明了一个函数,并且将这个函数内部的函数返回到全局 2 将这个返回到全局的函数内中的函数存储到全局变量中 3 内部的函数调用了外部函数中的局部变量 闭包简述: 有权访问另一个函数局部 ...

  8. JavaScript 反柯里化

    浅析 JavaScript 中的 函数 uncurrying 反柯里化 柯里化 柯里化又称部分求值,其含义是给函数分步传递参数,每次传递参数后部分应用参数,并返回一个更具体的函数接受剩下的参数,这中间 ...

  9. 浅析 JavaScript 中的 函数 uncurrying 反柯里化

    柯里化 柯里化又称部分求值,其含义是给函数分步传递参数,每次传递参数后部分应用参数,并返回一个更具体的函数接受剩下的参数,这中间可嵌套多层这样的接受部分参数函数,直至返回最后结果. 因此柯里化的过程是 ...

随机推荐

  1. FreeSql (十八)导航属性

    导航属性是 FreeSql 的特色功能之一,可通过约定配置.或自定义配置对象间的关系. 导航属性有 OneToMany, ManyToOne, ManyToMany, OneToOne, Parent ...

  2. 由于找不到opencv_world***d.dl,无法继续执行代码。重新安装程序可能会解决此问题。关于opencv使用imshow函数闪退解决方法等问题

    1.将缺失的文件放到程序根目录的debug中 2.将Debug x64下的附加依赖项改为只有后缀为d.lib的那个库文件,去除另一个,问题就这么解决了,虽然我也不知道加上另一个为什么就会闪退

  3. 初学FPGA图像处理,开发板选择建议

    我用的是ZYNQ7010的开发板,纯粹是入门学习,对于初学者,使用较多的xlinx入门级的开发板一般是zynq7000系列,淘宝上买的较好的是黑金和米联科,我买的就是黑金的,个人觉得教程很少,学习资料 ...

  4. 装系统 ------ 使用微PE 做系统盘

    1.什么是PE系统 pe系统是一种装系统的系统,也就是预装系统的系统,它是一种系统预装环境和工具. 可以放在U盘或光盘里随身携带,可以用来给电脑装系统 2.常见的制作pe 系统的工具 大白菜,U启动, ...

  5. 怎么在本地建立一个Maven 项目push到码云(https://git.oschina.net)

    本地建立一个的mvan项目不使用SmartGit push到码云上. 1 首先在自己码云的建立一个maven 空项目 2 然后打开STS(Spring Tool Suite)   新建一个Maven( ...

  6. sersync 实时同步

    1.什么是实时同步 ​ 监控一个目录的变化, 当该目录触发事件(创建\删除\修改) 就执行动作, 这个动作可以是 rsync同步 ,也可以是其他. 2.为什么要实时同步 1.能解决nfs单点故障问题. ...

  7. 表达式树练习实践:C#值类型、引用类型、泛型、集合、调用函数

    目录 表达式树练习实践:C#值类型.引用类型.泛型.集合.调用函数 一,定义变量 二,访问变量/类型的属性字段和方法 1. 访问属性 2. 调用函数 三,实例化引用类型 四,实例化泛型类型于调用 五, ...

  8. iOS渠道追踪统计方法大全

    说起 iOS 的渠道统计,不少人会想到苹果官方的 App 分析功能(iTunes Connect),但实际操作中我们会发现,这个服务的统计维度还不够全面,许多广告主和运营人员更关心的是各个推广渠道实际 ...

  9. JavaScript之对象Array

    数组Array是JavaScript中最常用的类型之一,同一数组中可以保存任意类型的数据,并且它的长度是动态的,会随着数组中数据的加减自动变化.每个数组都有一个表示其长度(数组元素的个数)的lengt ...

  10. 3分钟掌握GIt常用命令

    一.常用命令 git config [-l] 配置 git --help  帮助 git diff 文件  比较文件修改的内容 git add . 添加当前目录所有文件到暂存区 git add --u ...