关于前端:JavaScript-中的函数式编程函数组合和柯里化

41次阅读

共计 6189 个字符,预计需要花费 16 分钟才能阅读完成。

作者:Fernando Doglio
译者:前端小智
起源:medium

有幻想,有干货,微信搜寻 【大迁世界】 关注这个在凌晨还在刷碗的刷碗智。

本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试残缺考点、材料以及我的系列文章。

常常有读者问我,智哥,你 VSCode 主题是啥,好好看哦,能不能分享一下。刚好,明天 (周天) 看到 10 个难看的 VSCdoe 主题,其中第一个就是我应用的,这里分享给大家。

面向对象编程 函数式编程 是两种十分不同的编程范式,它们有本人的规定和优缺点。

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

高阶函数

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

高阶函数意味着函数不仅仅是一个能够从代码中定义和调用,实际上,你能够将它们用作可调配的实体。如果你应用过一些 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,如下:

当初咱们须要有个函数将传入的参数先加上 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

Elm 中 << 示意应用组合函数,在上例中 value 传给函数 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 循环这种反复的形式,对数组做一些常见的操作。但在函数式语言中只有递归没有循环,这三个函数就更有用了。附带提一句,在函数式语言中,递归函数不仅十分有用,还必不可少。


代码部署后可能存在的 BUG 没法实时晓得,预先为了解决这些 BUG,花了大量的工夫进行 log 调试,这边顺便给大家举荐一个好用的 BUG 监控工具 Fundebug。

原文:https://blog.bitsrc.io/functi…

交换

有幻想,有干货,微信搜寻 【大迁世界】 关注这个在凌晨还在刷碗的刷碗智。

本文 GitHub https://github.com/qq44924588… 已收录,有一线大厂面试残缺考点、材料以及我的系列文章。

正文完
 0