购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

1.3 理解函数式编程

函数式编程(Functional Programming,FP)是一种编程范式,它在现代前端领域的重要性已经超过了面向对象编程。

函数式编程以函数的使用为核心,是一种软件开发风格。在这种范式中,函数与函数的组合成为核心。通过将一系列具有特定输入输出的函数进行组合,可以得到最终的结果。

1.3.1 函数式的内涵

函数式编程具有以下三个特征:

● 保持纯函数,拒绝副作用。

● 函数是“一等函数”。

● 不可变值。

这三个特征是函数式编程的核心内容,所有关于函数式编程的讨论基本上都是围绕这三个特征展开的。

1.纯函数与副作用

我们可以通过以下两个特征来辨别一个函数是否为纯函数:

● 对于相同的输入,总是会得到相同的输出。

● 在执行过程中,没有产生副作用。

通过上述两点,我们可以更深入地理解纯函数的概念。纯函数是指在函数执行过程中,我们期望它仅根据输入的主键来安全准确地执行逻辑,在得到结果后输出,并且不会对函数的内部或外部产生任何预料之外的影响。举个小例子:

在上述代码中,函数外部的变量b被用作add函数计算的一部分。这意味着b的值会直接影响add函数的输出结果。因此,即使参数a的值保持不变,由于b值的变化,add函数的输出结果也可能不同。

2.一等函数

在JavaScript中,函数被视为“一等函数(First-class Function)”。这个概念在初次接触时可能让人感到困惑,不过一旦理解,其实质是非常直观的。所谓“一等函数”,是指函数在该语言中可以像变量一样使用。

那么,“像变量一样使用”具体指的是什么呢?简而言之,函数可以作为其他函数的参数、函数的返回值,或被赋值给一个变量。满足这三个条件的函数,就被认为是该语言中的“一等函数”。

上述代码很好理解,两个打印的add函数实际上都传入了一个函数作为参数。只是在第二个例子中,我们将这个函数赋值给了一个变量。test函数内部返回了一个函数,当我们执行常量t时,实际上就是在执行test函数内部返回的那个函数。

3.不可变值

如果要基于JavaScript这门语言来聊聊不可变值,就不得不聊聊JavaScript的数据类型。想必读者对以下数据类型早已耳熟能详:String、Number、Boolean、Null、undefined、Symbol、Object,以及新增加的BigInt,但这些不影响我们接下来的讨论。

这些数据类型可以分为值类型和引用类型,其中值类型往往是不可变的,而引用类型则可以改变。为什么会这样呢?因为值类型是按值访问的,而引用类型是按引用地址访问的。

我们来看一个值类型的小例子:


    var a = 1;
    var b = a;
    console.log(a === b); // true
    b = 2;
    console.log(a, b, a === b); // 1,2,false

可以看到,a和b是独立的、互不影响。再来看一个引用类型的例子:

你会发现,无论怎样更改.name的值,对象nameA和nameB的name属性值都会同步更新。这是因为当你将nameA赋值给nameB时,实际上是将nameA的引用地址赋值给了nameB,而不是创建一个全新的对象拷贝。因此,nameA和nameB实际上指向的是同一个对象。

像值类型这样的数据,我们称之为不可变数据或不可变值。而像对象这样的数据,由于其值是通过引用地址访问的,我们称之为可变数据。

在函数式编程中,可变数据可能会引发许多难以预料的副作用。举个简单的例子,假设有两个函数同时修改同一个对象。如果后执行的函数在执行时错误地假设自己所依赖的数据是第一个函数修改之前的数据(即原始对象数据),就可能引发一些看似隐蔽的错误。

因此,在函数式编程中,不可变数据扮演着至关重要的角色。当然,我们可以通过某些方法使引用的数据变得不可变,例如深拷贝等,但这里就不再赘述了。

1.3.2 组合与管道

组合(Compose)是函数式编程中的核心概念,甚至在面向对象编程领域,也兴起了一场“组合优于继承”的运动。

那么,为什么我们更倾向于使用组合而不是继承呢?原因在于继承可能会引发一系列复杂的问题。过度依赖继承会导致代码结构变得复杂且脆弱。特别是当继承层次过深,以至于不得不继承一些子类并不需要的方法或属性时,代码就会变得难以维护和控制。

而组合所拥有的一些特性可以在一定程度上避免这些问题:

(1)灵活性:组合为类之间的关系带来了更高的灵活性。由于组合不会造成紧密的耦合,一个类可以通过包含另一个类的实例作为其成员变量,使得类之间的关系更加松散。这样一来,类之间的依赖变得更容易调整,同时也便于替换或修改这些实例,增强了代码的可维护性和可扩展性。

(2)可维护性:继承往往会导致类之间产生深层次的依赖关系,这种依赖关系会使得代码变得更加复杂,难以理解和维护。相比之下,使用组合可以有效地减少这种依赖性,从而使得代码结构更加清晰、灵活,更易于维护和修改。

(3)多样性:组合使得类的行为更容易组合和重用。通过组合,可以将不同的类组合在一起,创造出具有新行为的新类,而无须继承整个类层次结构。这种灵活性使得我们能够更加高效地构建和维护复杂的系统。

在现代JavaScript开发中,诸如Vue、React等杰出的框架纷纷倡导函数式编程的理念。它们将props视为函数的参数,并将组件本身视作一个函数。最终,这个组件函数会返回一个模板,作为视图渲染的结果呈现。

在深入组合之前,我们需要先熟悉管道的概念,因为它是实现组合的基础。

在函数式编程中,管道(pipe)是一种将多个函数组合在一起,使一个函数的输出成为下一个函数的输入的方法。它允许你通过将数据从一个函数传递到另一个函数来创建数据处理流程。管道的概念源于Unix系统中的管道操作符“|”,它允许你将一个命令的输出作为另一个命令的输入。

换句话说,我们可以想象“水”流过“水管”的过程。如果水管足够长,就需要一节一节的水管首尾相连,既保证整个管道的密封性,又拓展了整个水管的长度,允许水流从一节一节的水管中流过,并在管道的最末尾流出。我们可以把一节一节的水管理解成函数,水就是数据(即传给函数的参数),而最后从水管中流出的水就是函数的返回值。

现在,让我们通过一个简单的管道代码示例来理解它:

核心的pipe函数虽然只有8行代码,但在函数式编程领域却蕴含着极其深刻的意义。它不仅是你踏入函数式编程世界的起点,更是理解其精髓的关键所在。现在,我们暂且不深入探究pipe函数的内部实现,先来看看它的实际应用场景。

pipe函数能够接收任意数量的函数作为参数,并返回一个新的函数。这个新函数会接收一个参数,作为内部计算的起始值。深入了解pipe函数,我们会发现其核心实际上是基于数组的reduce方法。关于reduce的使用,这里不再赘述;如果读者对此还不太熟悉,建议查阅MDN文档以获取更详尽的解答。

回到正题,通过pipe函数返回的函数,我们可以观察到传入的参数被用作reduce的初始值,即reduce的第二个参数。这里的关键点在于reduce的第一个参数——一个回调函数。这个回调函数实际上接收两个参数(尽管这种表述可能不够准确,但并不影响理解):一个是经过前一个回调函数计算后返回的值,另一个表示当前执行到了funcs数组的哪一个元素。在回调函数的内部,我们直接将上一次执行的结果作为当前函数的入参,这样就形成了一个类似流水线的处理过程。

值得一提的是,实现pipe函数并不一定要依赖于reduce。我们还可以使用循环、递归等其他方法来实现相同的功能,思路是相通的。读者可以尝试自行实现一下。

至于compose函数的实现,一旦理解了pipe的原理,其实非常简单。只需将reduce替换为reduceRight即可,也就是实现一个倒序的pipe。 xqTGujI+tlau7PrDLI0o+G9ZAzMKIcLTvDavYdem5kY+eaY/oCp+0YDJE+epxlWr

点击中间区域
呼出菜单
上一章
目录
下一章
×

打开