本文概述
函数式编程是使用表达式和函数来构建计算机程序而不改变状态和数据的范例。
通过遵守这些限制, 函数式编程的目的是编写更易于理解且更耐错误的代码。这是通过避免使用流控制语句(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)
映射器是一个函数, 它将数组的一项作为输入并返回输出。
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]]
对象分配
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!!!
局部套用
局部套用意味着将一个函数分解成一个或多个高阶函数级别的多个参数。
让我们来看看添加功能。
const add = (a, b) => a + b;
当我们要局部套用它时, 我们将其重写, 将参数分配到多个级别, 如下所示。
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());
我们还可以重新格式化局部套用函数, 使其看起来简洁。这是因为currying函数调用的每个级别都是单行return语句。因此, 我们可以在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提供了比以往更好的功能编程体验。