函数式编程简介:JavaScript 范式
已发表: 2022-03-11函数式编程是一种使用表达式和函数构建计算机程序而不改变状态和数据的范例。
通过尊重这些限制,函数式编程旨在编写更易于理解且更能抵抗错误的代码。 这是通过避免使用流控制语句( for
、 while
、 break
、 continue
、 goto
)来实现的,这些语句会使代码更难理解。 此外,函数式编程要求我们编写纯粹的、确定性的函数,这些函数不太可能出错。
在本文中,我们将讨论使用 JavaScript 进行函数式编程。 我们还将探索使之成为可能的各种 JavaScript 方法和功能。 最后,我们将探索与函数式编程相关的不同概念,并了解它们为何如此强大。
然而,在进入函数式编程之前,需要了解纯函数和不纯函数之间的区别。
纯函数与非纯函数
纯函数接受一些输入并给出固定输出。 此外,它们对外界没有副作用。
const add = (a, b) => a + b;
这里, add
是一个纯函数。 这是因为,对于a
和b
的固定值,输出将始终相同。
const SECRET = 42; const getId = (a) => SECRET * a;
getId
不是纯函数。 原因是它使用全局变量SECRET
来计算输出。 如果SECRET
发生变化, getId
函数将为相同的输入返回不同的值。 因此,它不是一个纯函数。
let id_count = 0; const getId = () => ++id_count;
这也是一个不纯的函数,这也有几个原因——(1)它使用一个非局部变量来计算它的输出,(2)它通过修改其中的一个变量在外部世界中产生副作用世界。
如果我们必须调试此代码,这可能会很麻烦。
id_count
的当前值是多少? 还有哪些其他函数正在修改id_count
? 还有其他依赖id_count
的功能吗?
由于这些原因,我们在函数式编程中只使用纯函数。
纯函数的另一个好处是它们可以并行化和记忆化。 看看前面的两个函数。 不可能并行化或记忆它们。 这有助于创建高性能代码。
函数式编程的原则
到目前为止,我们已经了解到函数式编程依赖于一些规则。 它们如下。
- 不要改变数据
- 使用纯函数:固定输出固定输入,无副作用
- 使用表达式和声明
当我们满足这些条件时,我们可以说我们的代码是正常的。
JavaScript 中的函数式编程
JavaScript 已经有一些启用函数式编程的函数。 示例:String.prototype.slice、Array.protoype.filter、Array.prototype.join。
另一方面,Array.prototype.forEach、Array.prototype.push 是不纯函数。
有人可能会争辩说Array.prototype.forEach
在设计上并不是一个不纯的函数,但请仔细想想——除了改变非本地数据或产生副作用之外,它不可能做任何事情。 因此,可以将其归入不纯函数的范畴。
此外,JavaScript 有一个 const 声明,非常适合函数式编程,因为我们不会改变任何数据。
JavaScript 中的纯函数
下面我们来看看 JavaScript 给出的一些纯函数(方法)。
筛选
顾名思义,这会过滤数组。
array.filter(condition);
这里的条件是一个获取数组每一项的函数,它应该决定是否保留该项并为此返回真实的布尔值。
const filterEven = x => x%2 === 0; [1, 2, 3].filter(filterEven); // [2]
注意filterEven
是一个纯函数。 如果它是不纯的,那么它会使整个过滤器调用不纯。
地图
map
将数组的每一项映射到一个函数,并根据函数调用的返回值创建一个新数组。
array.map(mapper)
mapper
是一个函数,它将数组的一项作为输入并返回输出。
const double = x => 2 * x; [1, 2, 3].map(double); // [2, 4, 6]
减少
reduce
将数组缩减为单个值。
array.reduce(reducer);
reducer
是一个函数,它获取累加值和数组中的下一项并返回新值。 对数组中的所有值一个接一个地调用它。
const sum = (accumulatedSum, arrayItem) => accumulatedSum + arrayItem [1, 2, 3].reduce(sum); // 6
康卡特
concat
将新项目添加到现有数组以创建新数组。 它与push()
的不同之处在于push()
会改变数据,从而使其不纯。
[1, 2].concat([3, 4]) // [1, 2, 3, 4]
您也可以使用扩展运算符执行相同操作。
[1, 2, ...[3, 4]]
对象.assign
Object.assign
将值从提供的对象复制到新对象。 由于函数式编程基于不可变数据,我们使用它来基于现有对象创建新对象。

const obj = {a : 2}; const newObj = Object.assign({}, obj); newObj.a = 3; obj.a; // 2
随着 ES6 的出现,这也可以使用扩展运算符来完成。
const newObj = {...obj};
创建自己的纯函数
我们也可以创建我们的纯函数。 让我们来复制一个字符串n
次。
const duplicate = (str, n) => n < 1 ? '' : str + duplicate(str, n-1);
此函数将一个字符串复制n
次并返回一个新字符串。
duplicate('hooray!', 3) // hooray!hooray!hooray!
高阶函数
高阶函数是接受函数作为参数并返回函数的函数。 通常,它们用于添加功能的功能。
const withLog = (fn) => { return (...args) => { console.log(`calling ${fn.name}`); return fn(...args); }; };
在上面的示例中,我们创建了一个withLog
高阶函数,它接受一个函数并返回一个函数,该函数在包装函数运行之前记录一条消息。
const add = (a, b) => a + b; const addWithLogging = withLog(add); addWithLogging(3, 4); // calling add // 7
withLog
HOF 也可以与其他函数一起使用,并且它可以在没有任何冲突或编写额外代码的情况下工作。 这就是 HOF 的美妙之处。
const addWithLogging = withLog(add); const hype = s => s + '!!!'; const hypeWithLogging = withLog(hype); hypeWithLogging('Sale'); // calling hype // Sale!!!
也可以在不定义组合函数的情况下调用它。
withLog(hype)('Sale'); // calling hype // Sale!!!
咖喱
柯里化意味着将一个接受多个参数的函数分解为一个或多个级别的高阶函数。
让我们来看看add
功能。
const add = (a, b) => a + b;
当我们要对它进行 curry 时,我们重写它,将参数分配到多个级别,如下所示。
const add = a => { return b => { return a + b; }; }; add(3)(4); // 7
柯里化的好处是记忆。 我们现在可以记住函数调用中的某些参数,以便以后可以重用它们而无需重复和重新计算。
// assume getOffsetNumer() call is expensive const addOffset = add(getOffsetNumber()); addOffset(4); // 4 + getOffsetNumber() addOffset(6);
这肯定比在任何地方都使用这两个参数要好。
// (X) DON"T DO THIS add(4, getOffsetNumber()); add(6, getOffsetNumber()); add(10, getOffsetNumber());
我们还可以重新格式化我们的 curried 函数以使其看起来简洁。 这是因为柯里化函数调用的每一级都是单行返回语句。 因此,我们可以使用 ES6 中的箭头函数对其进行重构,如下所示。
const add = a => b => a + b;
作品
在数学中,组合被定义为将一个函数的输出传递给另一个函数的输入,从而创建一个组合输出。 由于我们使用的是纯函数,因此在函数式编程中也是如此。
为了展示一个例子,让我们创建一些函数。
第一个函数是 range,它接受一个起始数字a
和一个结束数字b
并创建一个由从a
到b
的数字组成的数组。
const range = (a, b) => a > b ? [] : [a, ...range(a+1, b)];
然后我们有一个乘法函数,它接受一个数组并将其中的所有数字相乘。
const multiply = arr => arr.reduce((p, a) => p * a);
我们将一起使用这些函数来计算阶乘。
const factorial = n => multiply(range(1, n)); factorial(5); // 120 factorial(6); // 720
上述计算阶乘的函数类似于f(x) = g(h(x))
,从而证明了组合属性。
结束语
我们学习了纯函数和非纯函数、函数式编程、有助于它的新 JavaScript 特性,以及函数式编程中的一些关键概念。
我们希望这篇文章能激起您对函数式编程的兴趣,并可能激励您在代码中尝试它。 我们确信这将是一次学习体验,也是您软件开发之旅中的里程碑。
函数式编程是一种经过充分研究且健壮的计算机程序编写范例。 随着 ES6 的引入,JavaScript 提供了比以往更好的函数式编程体验。