重点摘要
1. JavaScript通过一等函数支持函数式编程
一等函数是指可以像其他值一样随处使用的函数——几乎没有限制。
函数作为值。 在JavaScript中,函数可以赋值给变量,作为参数传递给其他函数,从函数中返回,并存储在数据结构中。这种灵活性使得函数式编程技术成为可能。例如:
- 将函数赋值给变量:
var square = function(x) { return x * x; }
- 将函数作为参数传递:
[1, 2, 3].map(function(x) { return x * 2; })
- 返回一个函数:
function makeAdder(x) { return function(y) { return x + y; }; }
函数的一等性质是JavaScript中许多函数式编程模式的基础。
2. 高阶函数是函数式JavaScript的关键
捕获其他函数的高阶函数是构建抽象的非常强大的技术。
强大的抽象。 高阶函数要么将函数作为参数,要么返回函数作为结果。它们使得强大的抽象和代码重用成为可能。常见的例子包括:
- map:转换集合中的每个元素
- reduce:将集合的元素组合成一个值
- filter:根据谓词选择集合中的元素
这些函数允许你简洁地表达复杂的操作并组合行为。例如:
const numbers = [1, 2, 3, 4, 5];
const evenSquares = numbers
.filter(x => x % 2 === 0)
.map(x => x * x);
这段代码清晰地表达了选择偶数并将其平方的意图,而无需显式管理循环或中间数组。
3. 闭包在JavaScript中实现强大的函数式技术
闭包是一个捕获其定义范围内的外部绑定(即,不是其自身参数)的函数,以便在以后使用(即使在该范围完成之后)。
封装和状态。 闭包允许函数“记住”并访问其外部范围的变量,即使在该范围执行完毕之后。这使得:
- 私有状态:创建仅通过特定函数访问的变量
- 函数工厂:根据参数生成专门的函数
- 部分应用:通过固定现有函数的一些参数创建新函数
维护私有状态的闭包示例:
function counter() {
let count = 0;
return function() {
return ++count;
};
}
const increment = counter();
increment(); // 1
increment(); // 2
count
变量不能直接访问,但返回的函数可以访问和修改它。
4. 函数组合允许从简单部分构建复杂行为
函数式编程是关于将程序拆开并从相同的部分重新组装起来,抽象在函数边界后面。
构建复杂性。 函数组合是将两个或多个函数组合成一个新函数的过程。这允许你:
- 从简单、可重用的部分创建复杂行为
- 通过分解复杂操作提高代码可读性
- 通过隔离和测试较小的功能单元提高可维护性
组合可以通过各种方式实现:
- 手动组合:
const f = x => h(g(x))
- 工具函数:
const compose = (f, g) => x => f(g(x))
- 像Ramda或Lodash/FP这样的库
通过组合构建数据处理管道的示例:
const processData = compose(
summarize,
filterOutliers,
normalize,
parseInput
);
这清晰地表达了数据处理的步骤,而不会使代码充满实现细节。
5. 纯函数和不可变性导致更可预测的代码
使用纯函数编程可能看起来非常有限。[...] 然而,当你对状态变异采取自由主义观点时,你实际上是在限制组合的可能性,复杂化你推理任何给定语句效果的能力,并使测试代码变得更加困难。
可预测性和可测试性。 纯函数对于给定的输入总是产生相同的输出,并且没有副作用。这与不可变数据结合:
- 简化了对代码行为的推理
- 促进了更容易的测试和调试
- 使并行化和记忆化变得安全
保持纯度和不可变性的策略:
- 对不应改变的变量使用
const
- 创建新对象/数组而不是修改现有的
- 使用像Immutable.js这样的库来实现高效的不可变数据结构
纯函数的示例:
function addToCart(cart, item) {
return [...cart, item];
}
这个函数不会修改原始购物车,使得跟踪变化和预测行为变得更容易。
6. 递归在函数式编程中提供了循环的替代方案
使用递归,状态通过函数参数管理,变化通过从一个递归调用到下一个递归调用的参数建模。
优雅的解决方案。 递归通常为涉及层次结构或重复结构的问题提供更优雅和简洁的解决方案。好处包括:
- 一些算法的自然表达(例如,树遍历)
- 避免与循环相关的可变状态
- 编译器优化的潜力(尾调用优化)
然而,要注意JavaScript中的堆栈溢出风险,大多数环境中缺乏尾调用优化。缓解此问题的技术包括:
- 跳板:将递归调用包装在thunks中
- 续延传递风格:显式管理调用栈
将嵌套数组展平的递归函数示例:
function flatten(arr) {
return arr.reduce((flat, next) =>
flat.concat(Array.isArray(next) ? flatten(next) : next),
[]);
}
7. 函数式编程促进数据流和转换管道
管道应该是纯粹的——没有数据因通过管道而受到损害。
清晰的数据转换。 函数式编程鼓励以数据通过一系列转换的方式思考。这种方法:
- 通过清晰展示数据处理的步骤提高代码可读性
- 通过分离关注点提高可维护性
- 促进并行化和优化
构建管道的技术:
- 方法链(例如,使用lodash)
- 纯函数的组合
- 专门的库,如RxJS用于异步数据流
数据处理管道的示例:
const processOrders = pipe(
fetchOrders,
filterValidOrders,
calculateTotals,
generateReport
);
这清晰地展示了处理订单的步骤,而不会陷入实现细节。
8. 基于Mixin的设计提供了对象组合的函数式方法
简单的数据是最好的。专门的数据类型应该是特殊的。
灵活的组合。 Mixin提供了一种在不依赖于经典继承的情况下组合对象行为的方法。这种方法:
- 允许更灵活和模块化的代码设计
- 避免了与深层继承层次结构相关的问题
- 促进了面向对象编程的函数式风格
在JavaScript中实现Mixin:
- 使用
Object.assign()
将方法复制到对象原型上 - 使用高阶函数创建应用Mixin的工厂函数
创建具有Mixin的对象的示例:
const withLogging = (obj) => ({
...obj,
log: (msg) => console.log(`[${obj.name}]: ${msg}`)
});
const withValidator = (obj) => ({
...obj,
validate: () => { /* validation logic */ }
});
const createUser = (name) =>
withValidator(withLogging({ name, data: {} }));
这种方法允许灵活地组合对象行为,而无需僵化的类层次结构。
最后更新日期:
评论
《Functional JavaScript》获得了大多数正面评价,平均评分为4.07分(满分5分)。读者们赞赏书中对JavaScript中函数式编程概念的清晰解释,称赞其直截了当的方式和实用的例子。许多人认为这本书让他们大开眼界,对提升编程技能非常有价值。一些评论者指出书中的内容较为密集,建议中级到高级程序员阅读。尽管有少数人批评其对Underscore.js的依赖,并质疑其在实际应用中的适用性,但大多数人认为这是一本关于JavaScript中函数式编程技术的扎实入门书籍。