首页 > web前端 > js教程 > 正文

深入浅析JavaScript中的高阶函数、柯里化和组合函数

青灯夜游
发布: 2021-09-22 20:10:45
转载
2178人浏览过

本篇文章带大家了解一下javascript 中的函数式编程,介绍一下高阶函数、柯里化和组合函数,以及常见的函数式函数,希望对大家有所帮助!

深入浅析JavaScript中的高阶函数、柯里化和组合函数

面向对象编程函数式编程是两种非常不同的编程范式,它们有自己的规则和优缺点。

但是,JavaScript 并没有一直遵循一个规则,而是正好处于这两个规则的中间,它提供了普通OOP语言的一些方面,比如类、对象、继承等等。但与此同时,它还为你提供了函数编程的一些概念,比如高阶函数以及组合它们的能力。

高阶函数

我们行人人三个概念中最重要的一个开始:高阶函数。

立即学习Java免费学习笔记(深入)”;

高阶函数意味着函数不仅仅是一个可以从代码中定义和调用,实际上,你可以将它们用作可分配的实体。如果你使用过一些JavaScript,那么这并不奇怪。将匿名函数分配给常量,这样的事情非常常见。

const adder = (a, b) => {
  return a + b
}
登录后复制

上述逻辑在许多其他语言中是无效的,能够像分配整数一样分配函数是一个非常有用的工具,实际上,本文涵盖的大多数主题都是该函数的副产品。

高阶函数的好处:封装行为

有了高阶函数,我们不仅可以像上面那样分配函数,还可以在函数调用时将它们作为参数传递。这为创建一常动态的代码基打开了大门,在这个代码基础上,可以直接将复杂行为作为参数传递来重用它。

想象一下,在纯面向对象的环境中工作,你想扩展类的功能,以完成任务。 在这种情况下,你可能会使用继承,方法是将该实现逻辑封装在一个抽象类中,然后将其扩展为一组实现类。 这是一种完美的 OOP 行为,并且行之有效,我们:

  • 创建了一个抽象结构来封装我们的可重用逻辑
  • 创建了二级构造
  • 我们重用的原有的类,并扩展了它

现在,我们想要的是重用逻辑,我们可以简单地将可重用逻辑提取到函数中,然后将该函数作为参数传递给任何其他函数,这种方法,可以少省去一些创建“样板”过程,因为,我们只是在创建函数。

下面的代码显示了如何在 OOP 中重用程序逻辑。

//Encapsulated behavior封装行为stract class LogFormatter {
  
  format(msg) {
    return Date.now() + "::" + msg
  } 
}

//重用行为
class ConsoleLogger extends LogFormatter {
  
  log(msg) {
    console.log(this.format(msg))
  }  
}

class FileLogger extends LogFormatter {

  log(msg) {
    writeToFileSync(this.logFile, this.format(msg))
  }
}
登录后复制

第二个示是将逻辑提取到函数中,我们可以混合匹配轻松创建所需的内容。 你可以继续添加更多格式和编写功能,然后只需将它们与一行代码混合在一起即可:

// 泛型行为抽象
function format(msg) {
  return Date.now() + "::" + msg
}

function consoleWriter(msg) {
  console.log(msg)
}

function fileWriter(msg) {
  let logFile = "logfile.log"
  writeToFileSync(logFile, msg)
}

function logger(output, format) {
  return msg => {
    output(format(msg))
  }
}
// 通过组合函数来使用它
const consoleLogger = logger(consoleWriter, format)
const fileLogger = logger(fileWriter, format)
登录后复制

这两种方法都有优点,而且都非常有效,没有谁最优。这里只是展示这种方法的灵活性,我们有能力通过 行为(即函数)作为参数,就好像它们是基本类型(如整数或字符串)一样。

高阶函数的好处:简洁代码

对于这个好处,一个很好的例子就是Array方法,例如forEachmapreduce等等。 在非函数式编程语言(例如C)中,对数组元素进行迭代并对其进行转换需要使用for循环或某些其他循环结构。 这就要求我们以指定方式编写代码,就是需求描述循环发生的过程。

let myArray = [1,2,3,4]
let transformedArray = []

for(let i = 0; i < myArray.length; i++) {
  transformedArray.push(myArray[i] * 2) 
}
登录后复制

上面的代码主要做了:

  • 声明一个新变量i,该变量将用作myArray的索引,其值的范围为0myArray的长度
  • 对于i的每个值,将myArray的值在i的位置相乘,并将其添加到transformedArray数组中。

这种方法很有效,而且相对容易理解,然而,这种逻辑的复杂性会随着项目的复杂程度上升而上升,认知负荷也会随之增加。但是,像下面这种方式就更容易阅读:

const double = x => x * 2;

let myArray = [1,2,3,4];
let transformedArray = myArray.map(double);
登录后复制

与第一种方式相比,这种方式更容易阅读,而且由于逻辑隐藏在两个函数(mapdouble)中,因此你不必担心了解它们的工作原理。 你也可以在第一个示例中将乘法逻辑隐藏在函数内部,但是遍历逻辑必须存在,这就增加了一些不必要的阅读阻碍。

柯里化

函数柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。我们来看个例子:

function adder(a, b) {
  return a + b
}

// 变成
const add10 = x => adder(a, 10)
登录后复制

现在,如果你要做的就是将10添加到一系列值中,则可以调用add10而不是每次都使用相同的第二个参数调用adder。 这个事例看起来比较蠢,但它是体现了 柯里化 的理想。

你可以将柯里化视为函数式编程的继承,然后按照这种思路再回到logger的示例,可以得到以下内容:

function log(msg, msgPrefix, output) {
  output(msgPrefix + msg)
} 

function consoleOutput(msg) {
  console.log(msg)
}

function fileOutput(msg) {
  let filename = "mylogs.log"
  writeFileSync(msg, filename)
}

const logger = msg => log(msg, ">>", consoleOutput);
const fileLogger = msg => log(msg, "::", fileOutput);
登录后复制

log的函数需要三个参数,而我们将其引入仅需要一个参数的专用版本中,因为其他两个参数已由我们选择。

注意,这里将log函数视为抽象类,只是因为在我的示例中,不想直接使用它,但是这样做是没有限制的,因为这只是一个普通的函数。 如果我们使用的是类,则将无法直接实例化它。

组合函数

函数组合就是组合两到多个函数来生成一个新函数的过程。将函数组合在一起,就像将一连串管道扣合在一起,让数据流过一样。

在计算机科学中,函数组合是将简单函数组合成更复杂函数的一种行为或机制。就像数学中通常的函数组成一样,每个函数的结果作为下一个函数的参数传递,而最后一个函数的结果是整个函数的结果。

这是来自维基百科的函数组合的定义,粗体部分是比较关键的部分。使用柯里化时,就没有该限制,我们可以轻松使用预设的函数参数。

代码重用听起来很棒,但是实现起来很难。如果代码业务性过于具体,就很难重用它。如时代码太过通用简单,又很少人使用。所以我们需要平衡两者,一种制作更小的、可重用的部件的方法,我们可以将其作为构建块来构建更复杂的功能。

在函数式编程中,函数是我们的构建块。每个函数都有各自的功能,然后我们把需要的功能(函数)组合起来完成我们的需求,这种方式有点像乐高的积木,在编程中我们称为 组合函数。

看下以下两个函数:

var add10 = function(value) {
    return value + 10;
};
var mult5 = function(value) {
    return value * 5;
};
登录后复制

上面写法有点冗长了,我们用箭头函数改写一下:

var add10 = value => value + 10;
var mult5 = value => value * 5;
登录后复制

现在我们需要有个函数将传入的参数先加上 10 ,然后在乘以 5, 如下:

Find JSON Path Online
Find JSON Path Online

Easily find JSON paths within JSON objects using our intuitive Json Path Finder

Find JSON Path Online 193
查看详情 Find JSON Path Online

现在我们需要有个函数将传入的参数先加上 10 ,然后在乘以 5, 如下:

var mult5AfterAdd10 = value => 5 * (value + 10)
登录后复制

尽管这是一个非常简单的例子,但仍然不想从头编写这个函数。首先,这里可能会犯一个错误,比如忘记括号。第二,我们已经有了一个加 10 的函数  add10 和一个乘以 5 的函数 mult5 ,所以这里我们就在写已经重复的代码了。

使用函数 add10mult5 来重构 mult5AfterAdd10

var mult5AfterAdd10 = value => mult5(add10(value));
登录后复制

我们只是使用现有的函数来创建 mult5AfterAdd10,但是还有更好的方法。

在数学中, f ∘ g 是函数组合,叫作“f 由 g 组合”,或者更常见的是 “f after g”。 因此 (f ∘ g)(x) 等效于f(g(x)) 表示调用 g 之后调用 f

在我们的例子中,我们有 mult5 ∘ add10 或 “add10 after mult5”,因此我们的函数的名称叫做 mult5AfterAdd10。由于Javascript本身不做函数组合,看看 Elm 是怎么写的:

add10 value =
    value + 10
mult5 value =
    value * 5
mult5AfterAdd10 value =
    (mult5 << add10) value
登录后复制

Elmvalue 传给函数 add10 然后将其结果传递给 mult5。还可以这样组合任意多个函数:

f x =
   (g << h << s << r << t) x
登录后复制

这里 x 传递给函数 t,函数 t 的结果传递给 r,函数 t 的结果传递给 s,以此类推。在Javascript中做类似的事情,它看起来会像 g(h(s(r(t(x))))),一个括号噩梦。

常见的函数式函数(Functional Function)

函数式语言中3个常见的函数:Map,Filter,Reduce

如下JavaScript代码:

 for (var i = 0; i < something.length; ++i) {
    // do stuff
 }
登录后复制

这段代码存在一个很大的问题,但不是bug。问题在于它有很多重复代码(boilerplate code)。如果你用命令式语言来编程,比如Java,C#,JavaScript,PHP,Python等等,你会发现这样的代码你写地最多。这就是问题所在

现在让我们一步一步的解决问题,最后封装成一个看不见 for 语法函数:

先用名为 things 的数组来修改上述代码:

var things = [1, 2, 3, 4];
for (var i = 0; i < things.length; ++i) {
    things[i] = things[i] * 10; // 警告:值被改变!
}
console.log(things); // [10, 20, 30, 40]
登录后复制

这样做法很不对,数值被改变了!

在重新修改一次:

var things = [1, 2, 3, 4];
var newThings = [];
for (var i = 0; i < things.length; ++i) {
    newThings[i] = things[i] * 10;
}
console.log(newThings); // [10, 20, 30, 40]
登录后复制

这里没有修改things数值,但却却修改了newThings。暂时先不管这个,毕竟我们现在用的是 JavaScript。一旦使用函数式语言,任何东西都是不可变的。

现在将代码封装成一个函数,我们将其命名为 map,因为这个函数的功能就是将一个数组的每个值映射(map)到新数组的一个新值。

var map = (f, array) => {
    var newArray = [];
    for (var i = 0; i < array.length; ++i) {
        newArray[i] = f(array[i]);
    }
    return newArray;
};
登录后复制

函数 f 作为参数传入,那么函数 map 可以对 array 数组的每项进行任意的操作。

现在使用 map 重写之前的代码:

var things = [1, 2, 3, 4];
var newThings = map(v => v * 10, things);
登录后复制

这里没有 for 循环!而且代码更具可读性,也更易分析。

现在让我们写另一个常见的函数来过滤数组中的元素:

var filter = (pred, array) => {
    var newArray = [];
for (var i = 0; i < array.length; ++i) {
        if (pred(array[i]))
            newArray[newArray.length] = array[i];
    }
    return newArray;
};
登录后复制

当某些项需要被保留的时候,断言函数 pred 返回TRUE,否则返回FALSE。

使用过滤器过滤奇数:

var isOdd = x => x % 2 !== 0;
var numbers = [1, 2, 3, 4, 5];
var oddNumbers = filter(isOdd, numbers);
console.log(oddNumbers); // [1, 3, 5]
登录后复制

比起用 for 循环的手动编程,filter 函数简单多了。最后一个常见函数叫reduce。通常这个函数用来将一个数列归约(reduce)成一个数值,但事实上它能做很多事情。

在函数式语言中,这个函数称为 fold

var reduce = (f, start, array) => {
    var acc = start;
    for (var i = 0; i < array.length; ++i)
        acc = f(array[i], acc); // f() 有2个参数
    return acc;
});
登录后复制

reduce函数接受一个归约函数 f,一个初始值 start,以及一个数组 array

这三个函数,map,filter,reduce能让我们绕过for循环这种重复的方式,对数组做一些常见的操作。但在函数式语言中只有递归没有循环,这三个函数就更有用了。附带提一句,在函数式语言中,递归函数不仅非常有用,还必不可少。

英文原文地址:https://blog.bitsrc.io/functional-programming-in-functions-composition-and-currying-3c765a50152e

作者:Fernando Doglio

更多编程相关知识,请访问:编程视频!!

以上就是深入浅析JavaScript中的高阶函数、柯里化和组合函数的详细内容,更多请关注php中文网其它相关文章!

相关标签:
java速学教程(入门到精通)
java速学教程(入门到精通)

java怎么学习?java怎么入门?java在哪学?java怎么学才快?不用担心,这里为大家提供了java速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载
来源:segmentfault网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号