第 2 章 JavaScript 函数式编程 — 全书代码的风格宪章
第 2 章 JavaScript 函数式编程 — 全书代码的风格宪章
这一章讲什么: 函数式编程(FP)是什么、JavaScript 凭什么算函数式语言、 以及怎么写出「纯」的函数。它在全书链条里的位置:风格宪章——后面每一章的 数据结构实现都默认遵守这里立的规矩:优先纯函数、不改输入、用高阶函数做包装。 读完本章,你看后面任何一段实现代码,都能说出它为什么长那样。
1. 顶层全景
为什么用 FP ──→ JS 算不算函数式语言 ──→ 四块基石 ──→ 纯度怎么守 ──→ 守不住怎么办
│ │ │ │ │
五个诉求 没有精确定义, ① 函数是一等公民 不读全局变量 状态当参数传进来
(好懂/好修/ 按「支持常见 FP ② 声明式数组方法 不存内部状态 (避免状态)
好测/模块化/ 特性」算,JS 算 ③ 高阶函数 不改传入参数 不纯的东西当参数传
可复用) ④ 纯函数 (注入模式)
图说:前半章讲「是什么、有什么好」,后半章讲「怎么做到、做不到时怎么补救」。2. 主线例子:一个数组、一组谓词,走完整个声明式工具箱
原书在这一章几乎全程用同一个数组,我们照原书把它当主线例子1:
let myArray = [22, 9, 60, 12, 4, 56];
const under21 = (value) => value < 21; // 谓词:「是否小于 21」
const equal21 = (value) => value === 21; // 谓词:「是否恰好 21」谓词(predicate) 这个词先解释:它是一个返回 true/false 的函数,
用来回答「这个元素符不符合条件」。under21 就是「小于 21 吗」。
下面每一步都对这同一个数组操作,每一步旁边写出具体结果:
第 ① 步 过滤 .filter()——选出符合条件的,留新数组:
myArray.filter(under21); // [9, 12, 4]22 不满足(不小于 21)、9 满足、60 不满足、12 满足、4 满足、56 不满足。
结果是新数组 [9, 12, 4],原数组一动没动2。
第 ② 步 查找 .find() / .findIndex()——找第一个满足条件的:
myArray.find(under21); // 9 第一个小于 21 的元素
myArray.findIndex(under21); // 1 它在数组中的位置(从 0 数)
myArray.find(equal21); // undefined 没有恰好 21 的
myArray.findIndex(equal21); // -1 同上,位置惯例返回 -1第 ③ 步 整体判断 .some() / .every():
myArray.some(under21); // true 至少有一个小于 21
myArray.every(under21); // false 并非所有元素都小于 21
myArray.some(equal21); // false
myArray.every(equal21); // false第 ④ 步 变形 .map()——一对一换成新值:
myArray.map((x) => 10 * x); // [220, 90, 600, 120, 40, 560]每个元素各自乘 10,还是新数组。
第 ⑤ 步 归约 .reduce()——把全数组揉成一个值:
myArray.reduce((a, v) => a + v, 0); // 163a 是累加器(accumulator,从 0 出发),v 是当前元素。走一遍:
a=0 加 22 得 22,加 9 得 31,加 60 得 91,加 12 得 103,加 4 得 107,加 56 得 1633。
一步进阶:一次算出「和」和「个数」,把累加器升级成对象:
myArray.reduce((a, v) => ({ s: a.s + v, c: a.c + 1 }), { s: 0, c: 0 });
// { s: 163, c: 6 } —— 和 163,共 6 个元素第 ⑥ 步 遍历 .forEach()——只要副作用(比如累加到一个外部变量)时:
let sum = 0;
myArray.forEach((v) => { sum += v; });
sum; // 163走完六步,注意三件事:没有一步写过 for 循环;没有一步改过 myArray 本身;
每一步都是「说清楚要什么」,而不是「指挥机器一步步怎么做」——这就是下一节的主题。
3. 核心原理
3.1 为什么用 FP:五个诉求
作者开头就问:你想要代码有什么品质?他列了五条,每条都对应 FP 的一个具体机制4:
| 诉求 | FP 怎么满足它 |
|---|---|
| 好懂 | 代码写一遍、读几十遍;FP 倾向产出更短更干净的代码 |
| 好维护 | 函数互相独立,改一个函数不用担心碰坏别的 |
| 好测试 | 纯函数给定输入必有同样输出,单测直接喂输入验输出即可 |
| 模块化 | 目标就是写出可以独立修改的函数(高内聚、低耦合) |
| 可复用 | 函数自成一体,写好的函数哪里都能搬 |
作者随即自己泼了冷水:面向对象的代码同样能做到这些,FP 不是银弹; 他的立场是走中间路线,「深思熟虑的均衡混合通常是最好的解」5。 这句话要当真——后面章节该用对象的地方照样用对象(第 13 章的类就是)。
3.2 JavaScript 算不算函数式语言
讨论这个问题的人通常举 Haskell、Erlang、Scala,没人带 JavaScript。 作者的应对很务实:「函数式语言」从来没有精确定义,那么本书就自定一个口径: 只要支持常见的 FP 特性,就算函数式语言6。 按这个口径,JavaScript 支持:函数作为一等公民、数组函数、纯函数、高阶函数、递归, 并能避开副作用——算。他还补了一句历史注脚:FP 不是新潮玩意,还在广泛使用的 第二老的语言 LISP(1958 年,仅比 FORTRAN 晚一年)完全建立在 FP 上7。
3.3 基石一:函数是一等公民
一等公民(first-class) 是个精确的说法:函数可以像数字、字符串一样—— 存进变量、当参数传给别的函数、作为别的函数的返回值8。
书里给的例子是前端常见的 API 调用:axios.get("url").then((response) => { … })。
.then() 的参数是一个函数,传它的方式和传一个数字或数组完全一样。
第 12 章会把这个手法用在二叉树遍历上:
const preOrder = (tree, visit = (x) => console.log(x)) => {
if (tree !== null) {
visit(tree.key);
preOrder(tree.left, visit);
preOrder(tree.right, visit);
}
};遍历树的时候「访问每个节点」要干什么?不写死,由调用方传一个 visit 函数进来;
不传就默认是「打印出来」。算法的骨架与对节点的具体动作,就此分离——
这是全书反复出现的设计模式:第 6 章排序的比较、第 10 章列表的操作,全都是
「骨架固定、动作注入」。
顺带定义:收函数当参数、或返回函数的,叫高阶函数(higher-order function); 不收不返的,叫一阶函数。所有带回调的函数和所有数组方法,都是高阶函数9。
3.4 基石二:声明式编程与数组方法
声明式(declarative)对照命令式(imperative):命令式一步步指挥 「初始化空数组、设 i 为 0、循环、判断、push」;声明式只说 「给我过滤后的新数组」,怎么循环、怎么收集,是方法内部的事10。
第 2 节那条主线已经演示了六个方法的用法。这里把「什么时候用哪个」收成一张表
(findLast/findLastIndex 是从尾往前找的近年新增版本)11:
| 想要 | 用 | 主线例子的结果 |
|---|---|---|
| 符合条件的子集(新数组) | .filter(谓词) | [9, 12, 4] |
| 第一个符合条件的元素 | .find(谓词) | 9 |
| 第一个符合条件的位置 | .findIndex(谓词) | 1 |
| 有没有至少一个符合 | .some(谓词) | true |
| 是不是全都符合 | .every(谓词) | false |
| 每个元素换个值(新数组) | .map(函数) | [220, 90, 600, 120, 40, 560] |
| 全数组揉成一个值 | .reduce(二元函数, 初值) | 163 |
| 从右往左揉 | .reduceRight(...) | 加法结果相同 |
| 只求副作用地过一遍 | .forEach(函数) | sum 变 163 |
两个提醒。第一,找特定值而不用条件时,.includes()、.indexOf()、
.lastIndexOf() 更直接,只是它们不接受任意条件,不如上表灵活12。
第二,.reduce() 最好总是给初值:对空数组归约不给初值会运行时报错13。
关于性能争议,书里的态度原话收进脚注:即使数组方法真比手写循环慢,也无关紧要—— 除非你实测确认瓶颈在此,否则写更长、更容易出 bug 的循环没有意义14。
3.5 基石三:纯函数与副作用
纯函数(pure function):只依赖自己的参数、不产生任何副作用的函数。 「副作用」指:改程序状态、改变量、改对象——凡是「除了返回值之外还动了别的地方」, 都算15。
纯函数的好处,书里给了两条,都很硬:
- 同样输入必得同样输出——结果不依赖任何外部变量,外部变了也不会悄悄变;
- 调用它时不用提心吊胆——不用担心哪里的状态被碰坏,注意力只在参数上。
反过来说,纯函数不能依赖这些东西:随机数、当前时间、I/O(输入输出)的结果—— 因为它们都是「函数之外的变量」16。
副作用有三个主要来源,书里一节一个,这也是后半章的骨架:
来源一:全局状态。 函数读了外部的共享变量,就不再纯。 调试会变难:要理解函数为什么返回这个值,你得搞清整个程序的历史状态17。
来源二:内部状态。 函数体内有跨调用存活的变量,也会让「同样输入」得到不同输出。 书里由此解释了函数式程序员为什么本能地避开对象:对象把数据存起来供方法使用, 方法的结果就可能依赖内部属性,纯度自动失守18。
来源三:修改参数。 JavaScript 里,基本类型(数字、字符串等)按值传参,
而对象和数组按引用传参——函数里改参数,改的就是调用方手里的原物。
最坑的是 JavaScript 自带一堆「按定义就改输入」的方法:myArray.sort()、
pop()、splice() 全都地动原数组19。
书里还点了一个容易踩的坑:想从右往左归约,有人先 .reverse() 再 .reduce(),
结果原数组被翻了个面——副作用。近年标准补了不改输入的版本:
toSorted()、toReversed()、toSpliced()(补充:这些是 ES2023 新增,
书中已提及)20。
本来就纯不了的函数怎么办:调 API 拿每日新闻、用 Math.random() 掷随机数、
读当前时间、做任何 I/O——这些函数的整个意义就在于「每次结果不一样」。
console.log 也算:它不返回有用值,但改了屏幕上的东西21。
所以下一节的主题不是「消灭不纯」,而是「缩小不纯的范围」。
3.6 基石四(补救):高阶函数做包装
包装(wrapping) 是高阶函数最实用的用法:造一个新函数,保留原函数的全部功能, 外面附加一点行为,而原函数一行不动。
书里完整的例子是给函数加日志。直接改原函数的风险:它有多个 return,
你得每个都改,还可能误伤。包装器的写法:
const addLogging = (fn) => (...args) => {
console.log("Entering", fn.name, "with", ...args);
const toReturn = fn(...args);
console.log("Exiting", fn.name, "returning", toReturn);
return toReturn;
};
const sum2 = (a, b) => { console.log("Calculating..."); return a + b; };
addLogging(sum2)(22, 9);
// Entering sum2 with 22 9
// Calculating...
// Exiting sum2 returning 31走一遍:addLogging(sum2) 返回一个新函数;调它并给 (22, 9),
新函数先打印「进入 sum2,参数 22 和 9」,再把 22, 9 转给原 sum2,
它打印「Calculating...」并算出 31,新函数接着打印「返回 31」,把 31 交给调用者。
sum2 本身从头到尾没被碰过22。
这个模式的全书戏份很重:第 5 章的记忆化、本章习题的计时器,都是它的变奏。 判断标准一句话:「附加行为」和「原逻辑」分离,附加部分可以随时拆下来。
3.7 补救一:状态当参数传来传去
如果一个函数需要全局状态,规矩是:需要哪些状态,就当参数传进来; 如果它需要更新状态,不许自己改,而是把新状态算出来、作为返回值交出去, 由调用方决定去不去更新全局23。
这一下同时解决了测试难题:测试时不用先布置全局环境,喂初始状态、验返回的新状态即可。
3.8 补救二:注入模式
对于「真的需要不纯操作」的场合(发请求、掷随机数),书里给的手法叫注入(injection): 不纯的那个动作本身作为函数参数传进来:
const getData = (url) => axios.get(url); // 真正发请求的函数
doSomething(a, b, c, getData); // 把它注进去
function doSomething(x, y, z, getter) {
// ...用 getter("/some/url") 代替直接 axios.get(...)
}被注入方的内部不再直接碰全局的 axios,于是它自己变纯了——不纯被挤到了
调用现场。测试时注一个假函数(mock)进去,不用真的发请求24。
书里点破了这层窗户纸:这和「状态当参数」其实是同一招——
「直接用 axios.get() 本质是在用全局对象的方法,注入一个参数,函数就不必碰全局」。
3.9 声明式为什么更安全:把循环的 bug 清单摊开看
作者没有逐一枚举手写循环容易错在哪,但把「命令式写法」的样板摊出来, bug 清单自己就显形了。同一个过滤任务,两种写法并排:
// 命令式:自己指挥每一步
let newArray = [];
for (let i = 0; i < myArray.length; i++) {
if (myArray[i] < 21) {
newArray.push(myArray[i]);
}
}
// 声明式:只说要什么
let newArray = myArray.filter(under21);四行变一行,同时消掉了手写循环的全部经典出错点(补充,不在书里,来自通用知识; 原书只给了「更短、更少机会引入 bug」的结论25):
| 手写循环的经典坑 | 声明式写法里还存在吗 |
|---|---|
i 从 0 还是从 1 开始、< 还是 <= | 不存在——边界由方法内部处理 |
| 忘初始化输出数组、忘 push | 不存在——新数组自动产生 |
| 循环里顺手改了原数组 | 不存在——方法不改输入 |
提前 return/break 逻辑纠缠 | 不存在——控制流是方法的 |
书里那句「代码更短,所以引入 bug 的机会更少」(
fewer chances of introducing bugs)说的就是这个对照25。
还有个词值得记:.map() 这类方法产出新数组而不改原数组,
所以用它写的函数天然是 3.5 节意义上的纯函数——声明式与纯度在这里合流。
3.10 .reduce() 的两种步态:数字累加器与对象累加器
第 2 节第 ⑤ 步只走了数字版,这里把对象版也走完,顺便说清 reduce 的通用形状。 通用形状是:累加器 + 当前元素 → 新累加器,反复应用,直到数组耗尽:
初值 {s:0, c:0}
│ 元素 22:新累加器 {s:0+22, c:0+1} = {s:22, c:1}
│ 元素 9:{s:31, c:2}
│ 元素 60:{s:91, c:3}
│ 元素 12:{s:103, c:4}
│ 元素 4:{s:107, c:5}
│ 元素 56:{s:163, c:6}
▼
结果 {s:163, c:6} —— 和 163,个数 6
图说:每一步都是「旧累加器 + 一个元素 = 新累加器」。累加器是什么类型,由初值决定。
初值是数字就揉出数字,初值是对象就能同时累计多个量。这个「累加器形状」在全书会反复出现:第 6 章的一些排序、第 17 章图算法里的距离表,
本质都是 reduce 思路的展开。.reduceRight() 唯一的差别是从最后一个元素开始
往前走;书里提醒:别用「先 .reverse() 再 .reduce()」来 emulate 它——
reverse 就地改数组,是教科书级的副作用事故(3.5 节来源三)26。
3.11 注入模式再走一步:测试为什么是主要受益者
注入的例子 3.8 节给了骨架,这里把它与测试的关系说透,因为这是书里 「simplifies unit testing」的实际含义27:
没有注入时,doSomething 内部直接 axios.get()——单测一跑,真的发出 HTTP 请求:
慢、依赖网络、结果每天不同,测试没法写断言。注入之后:
// 生产环境:注入真的
doSomething(a, b, c, (url) => axios.get(url));
// 测试环境:注入假的
doSomething(a, b, c, (url) => Promise.resolve({ data: "固定假数据" }));被测函数的输入完全由测试控制,输出因此可断言。「不纯挤到边界上」的准确含义是: 不纯挤到「组装参数的那个地方」——而那个地方正是测试可以接管的地方。 这也是第 18 章函数式数据结构的思想先声:把「变化」集中到边界,内部全是纯的。
3.12 本章规矩在后续章节的落点
把「风格宪章」说实:本章立的规矩,后面具体落在哪。
| 本章的规矩 | 后面哪里执行它 |
|---|---|
| 谓词注入遍历骨架 | 第 12 章 preOrder(tree, visit):遍历时做什么由调用方定 |
| 比较器注入排序骨架 | 第 6 章所有排序:把 a > b 换成 compare(a,b) 即通用化 |
| 包装器加行为 | 第 5 章记忆化:闭包存缓存表,查表优先于计算 |
| 不改输入 | 第 6 章排序先 [...arr] 复制;第 18 章整章以此为题 |
| 状态当参数与返回值 | 第 18 章持久化数据结构:更新返回新版本,旧版本原样保留 |
| 注入不纯动作 | 第 8 章洗牌/采样:Math.random() 集中在一处,算法主体确定性 |
这张表是「宪章」二字的证据:后面章节不是「风格相似」,而是逐条执行。
4. 作者的判断与证据
- 「中间路线」是明确立场,不是骑墙:作者明说 FP 不是银弹、均衡混合最好, 并以「后面该用 OOP 处照样用」贯穿全书作证(第 13 章的类、第 3 章的对象实现)5。
- 「函数式语言无精确定义」是文献事实,作者的应对是自定义操作口径(支持常见 FP 特性就算), 并立即用它论证 JS 合格——逻辑自洽,但读者要知道这个「算」是本书的口径,不是学界共识6。
- 性能态度有明确条件句:「即使数组方法更慢也没关系」的前提是「除非实测它就是瓶颈」—— 这是工程经验之谈,无数据支撑,书里也没打算给数据14。
- 习题是本章证据链的一部分:8 道习题(2.1–2.8)逐个逼问概念边界——纯度判别、
包装器容错、
.map(parseInt)的经典坑、用.findIndex重写.some/.every—— 答案在书末,我们把要点收在第 5 节。
判断(我们的,不是书里的): 本章真正的贡献不是「FP 很好」这个结论 (这类结论满大街都是),而是把 FP 落成了 JavaScript 里可执行的检查清单: 读全局了吗?存内部状态了吗?改参数了吗?改不了就注进来。 这四问比任何范式宣言都好用,而且直接决定了第 6、10、18 章代码的形状。 如果错,会错在: 这套清单对「算法教学代码」是对的,但对 I/O 密集的真实应用, 全面遵守它的成本可能超过收益——作者其实也承认(中间路线),只是没有量化成本边界。
5. 边界与局限
- 不教你 Haskell 式类型系统:没有代数数据类型、模式匹配、单子这些「真·函数式」 硬核概念;全书的 FP 就是「JS 里的纯函数 + 高阶函数」这一实用子集。
- 并发、异步不在本章范围:
.then()只是当一等公民的例子带了一下, Promise、async/await 的系统讲解没有(第 1 章特性清单也没提)。 - 「比循环慢没关系」缺数据:没有 benchmark,只有工程经验;在超热循环里 (百万级元素的归约),方法调用的开销是真实存在的——这是补充(不在书里,来自通用知识)。
- 答案部分的取舍:书末「Chapter 2」答案是习题 2.1–2.8 的解答,其中 2.2/2.3
(日志/计时包装器)有完整代码,2.4(
.map(parseInt)之谜)的解释尤其值得看—— 我们在 6.1 节展开。
5.1 习题答案里最有教学价值的三处
① 2.1 纯还是不纯? 题:const PI = 3.14159265358979; const circumference = (r) => 2 * PI * r;
用了全局变量 PI,形式上是「不纯」。答案的说法很老练:既然 PI 是常量、
又有 const 守着没人改得了,「可以勉强接受」;但最好的解法是根本不造这个外部变量,
直接用 Math.PI——纯度的边界案例,标准答案是不留边界,直接消掉28。
② 2.4 .map(parseInt) 之谜。 [1,2,4,8,16,32].map(parseInt) 得到
[1, NaN, NaN, NaN, 1, 17]——为什么?答案拆得很细:.map() 给回调传三个参数
(当前元素、它的下标、整个数组);parseInt 收两个参数(要解析的串、进制),
第三个被忽略,第二个被当成了进制。于是:parseInt("1", 0)→1(0 按 10 处理);
parseInt("2", 1)→NaN(一进制不存在);parseInt("4", 2)→NaN(二进制没有 4);
parseInt("8", 3)→NaN;parseInt("16", 4)→1(4 进制里 16 是 1×4+6?不,等等——
4 进制只允许数字 0–3,这里答案原文说 16 被当四进制解析……实际上 parseInt("16",4) 取
到第一个非法字符为止,得 1);parseInt("32", 5)→17(5 进制:3×5+2=17)29。
教训:回调签名对不上时,JS 不会报错,只会静默地拿多余参数干别的——这正是 3.5 节
「改参数/暗参数」风险的一个干净变体。
③ 2.6–2.7 互相重写。 .none() 可以一行造出来:没有元素满足谓词 ⟺ 所有元素都满足
「谓词取反」——people.every(negate(isAdult)),其中 negate = (fn) => (...args) => !fn(...args);
而 .some() 本身也能用 .findIndex() 重写:findIndex(谓词) !== -1 就是 some,
findIndex(取反的谓词) === -1 就是 every30。这组答案的意思:这些方法不是
六个黑箱,而是同一个「按谓词扫描」骨架的六个出口。
5.2 答案走读:计时包装器 addTiming(习题 2.3)
习题 2.2/2.3 的答案代码是全书包装器模式最完整的样板,值得逐行走一遍。
addTiming 在 addLogging 之上只加一样东西:计时31:
const {performance} = require("perf_hooks"); // Node.js 里要手动引入 performance
const addTiming = (fn) => (...args) => {
const output = (text, name, tStart, tEnd) =>
console.log(`${name} - ${text} - Time: ${tEnd - tStart} ms`);
const tStart = performance.now(); // 调用前记下时刻
try {
const valueToReturn = fn(...args); // 原样调用原函数
output("normal exit", fn.name, tStart, performance.now());
return valueToReturn; // 返回值原样上交
} catch (thrownError) {
output("exception thrown", fn.name, tStart, performance.now());
throw thrownError; // 异常也原样重抛
}
};逐行走完要点有三个。第一,返回值和异常都原样透传——包装器对调用方完全透明,
调用方感觉不到自己用的是包装后的函数;这正是「包装」的合格线。
第二,两条出路(正常返回/抛异常)都要计时,只计一半会让异常路径的耗时永远成谜。
第三,performance.now() 是浏览器和 Node 都有的高精度计时器,
Node 端要 require("perf_hooks") 引入——这个细节答案里专门注了一行。
把 addTiming(quicksort)(bigArray) 和第 6 章排序章节连起来用,
就是原书「性能」主题(第 4 章)的最小测量工具。
5.3 答案走读:.map(parseInt) 六连错全表(习题 2.4)
这个坑值得把六个元素的遭遇列成一张全表(依据书末答案的逐条解释29):
| 元素(字符串) | 下标被当成进制 | parseInt 收到的调用 | 结果 | 为什么 |
|---|---|---|---|---|
| "1" | 0 | parseInt("1", 0) | 1 | 进制 0 按 10 处理 |
| "2" | 1 | parseInt("2", 1) | NaN | 一进制不存在 |
| "4" | 2 | parseInt("4", 2) | NaN | 二进制只有 0 和 1 |
| "8" | 3 | parseInt("8", 3) | NaN | 三进制最多到 2 |
| "16" | 4 | parseInt("16", 4) | 1 | 解析到第一个非法字符 6 为止 |
| "32" | 5 | parseInt("32", 5) | 17 | 5 进制合法:3×5+2=17 |
所以输出是 [1, NaN, NaN, NaN, 1, 17]——没有任何一步报错,
每一步都是「合法地」拿错了参数。这个例子在全书的教学价值在于:
它把 3.4 节那句「回调签名对不上时 JS 不报错」从警告变成了你见过一次的现场。
6. 可带走的
-
五个诉求是 FP 的卖点:好懂、好维护、好测试、模块化、可复用——但作者明说 OOP 同样能做到,别当宗教。
-
一等公民 = 函数能进变量、能当参数、能当返回值;高阶函数 = 收函数或返回函数的函数。
-
谓词是返回布尔的函数;
.filter/.find/.some/.every全是「按谓词扫描」一家子。 -
.map一对一变形、.reduce全组揉成一个值(记得给初值,空数组才不炸)。 -
纯函数检查清单:读全局?存内部状态?改参数?——三问都答「否」才算纯。
-
对象和数组按引用传参,函数里改它们就是改调用方的原物;
sort()/splice()这类方法本来就地改动。 -
包装器模式:
addLogging那种「原函数不动、套一层附加行为」的写法,是调试与计时、记忆化的通用骨架。 -
守不住纯度时:状态当参数传进来、新状态当返回值交出去;不纯的动作当函数注进来(注入模式),测试时换假函数。
-
.map(parseInt)的坑记住:回调会收到 (元素, 下标, 数组) 三个参数——签名对不上是静默灾难。 -
谓词三兄弟的关系:
some= findIndex ≠ -1;every= 取反后 findIndex = -1;none=every加取反——六个数组方法是一个骨架的六个出口(习题 2.7 答案)。
7. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 五个诉求、中间路线 | Why Use Functional Programming? | text/11-fm-why-use-functional-programming.txt:41(搜「Understandable」) · text/11-fm-why-use-functional-programming.txt:58(搜「silver bullet」) |
| FP 语言无定义、本书口径 | Why Use Functional Programming? | text/11-fm-why-use-functional-programming.txt:68(搜「precise definition」) |
| LISP 注脚 | Why Use Functional Programming? | text/11-fm-why-use-functional-programming.txt:29(搜「LISP」) |
| 一等公民、preOrder 例子 | Why Use Functional Programming? | text/11-fm-why-use-functional-programming.txt:76(搜「first-class」) · text/11-fm-why-use-functional-programming.txt:91(搜「preOrder」) |
| 高阶函数定义 | Why Use Functional Programming? | text/11-fm-why-use-functional-programming.txt:101(搜「higher-order」) |
| 声明式与数组方法清单 | Why Use Functional Programming? | text/11-fm-why-use-functional-programming.txt:128(搜「declarative」) · text/11-fm-why-use-functional-programming.txt:135(搜「filter()」) |
| 性能争议注 | Why Use Functional Programming? | text/11-fm-why-use-functional-programming.txt:172(搜「irrelevant」) |
| filter 主线例子 | Why Use Functional Programming? | text/11-fm-why-use-functional-programming.txt:186(搜「under21」) · text/11-fm-why-use-functional-programming.txt:192(搜「9, 12, 4」) |
| find/findIndex | Why Use Functional Programming? | text/11-fm-why-use-functional-programming.txt:206(搜「findLast」) |
| some/every | Why Use Functional Programming? | text/11-fm-why-use-functional-programming.txt:244(搜「every()」) |
| map | Why Use Functional Programming? | text/11-fm-why-use-functional-programming.txt:275(搜「10*x」) |
| reduce、初值警告 | Why Use Functional Programming? | text/11-fm-why-use-functional-programming.txt:295(搜「accumulator」) · text/11-fm-why-use-functional-programming.txt:295(搜「runtime error」) |
| forEach | Why Use Functional Programming? | text/11-fm-why-use-functional-programming.txt:317(搜「forEach()」) |
| 包装器 addLogging | Why Use Functional Programming? | text/11-fm-why-use-functional-programming.txt:368(搜「addLogging」) · text/11-fm-why-use-functional-programming.txt:385(搜「sum2」) |
| 纯函数定义 | Why Use Functional Programming? | text/11-fm-why-use-functional-programming.txt:399(搜「pure function」) |
| 纯函数不能依赖的 | Why Use Functional Programming? | text/11-fm-why-use-functional-programming.txt:402(搜「random numbers」) |
| 全局/内部状态两节 | Why Use Functional Programming? | text/11-fm-why-use-functional-programming.txt:410(搜「nonlocal」) · text/11-fm-why-use-functional-programming.txt:423(搜「between calls」) |
| 修改参数、mutator 方法、toSorted 系 | Why Use Functional Programming? | text/11-fm-why-use-functional-programming.txt:436(搜「by reference」) · text/11-fm-why-use-functional-programming.txt:439(搜「toSorted()」) |
| 本质不纯的函数 | Why Use Functional Programming? | text/11-fm-why-use-functional-programming.txt:449(搜「daily news」) |
| 状态当参数 | Why Use Functional Programming? | text/11-fm-why-use-functional-programming.txt:472(搜「updated state」) |
| 注入模式 | Why Use Functional Programming? | text/11-fm-why-use-functional-programming.txt:501(搜「getData」) |
| 习题 2.1–2.8 | Why Use Functional Programming? | text/11-fm-why-use-functional-programming.txt:537(搜「Pure or Impure」) |
| 答案 2.1(PI/Math.PI) | Chapter 2 | text/65-ch02-chapter-2.txt:7(搜「Math.PI」) |
| 答案 2.2/2.3(包装器完整代码) | Chapter 2 | text/65-ch02-chapter-2.txt:16(搜「addLogging」) · text/65-ch02-chapter-2.txt:38(搜「addTiming」) |
| 答案 2.4(map/parseInt) | Chapter 2 | text/65-ch02-chapter-2.txt:58(搜「parseInt()」) |
| 答案 2.5–2.7(negate、none、some/every 重写) | Chapter 2 | text/65-ch02-chapter-2.txt:67(搜「negate」) · text/65-ch02-chapter-2.txt:71(搜「none」) · text/65-ch02-chapter-2.txt:77(搜「findIndex」) |
Footnotes
-
出处:「Why Use Functional Programming?」第 190 段(
text/11-fm-why-use-functional-programming.txt:190,搜「myArray」)。原文:let myArray = [22, 9, 60, 12, 4, 56];,本章多数例子共用此数组。 ↩ -
出处:「Why Use Functional Programming?」第 191 段(
text/11-fm-why-use-functional-programming.txt:191,搜「filter(under21)」)。原文:let newArray = myArray.filter(under21);结果为[9, 12, 4];并说明「不需要写任何控制循环、初始化输出数组的代码」。 ↩ -
出处:「Why Use Functional Programming?」第 295 段(
text/11-fm-why-use-functional-programming.txt:295,搜「accumulator」)。原文:const mySum = myArray.reduce((a, v) => a + v, 0); // 163;a 是累加器(accumulator),v 是当前元素。 ↩ -
出处:「Why Use Functional Programming?」第 41–53 段(
text/11-fm-why-use-functional-programming.txt:41,搜「Understandable」)。原文列五个品质:可理解、可维护、可测试、模块化、可复用。 ↩ -
出处:「Why Use Functional Programming?」第 58 段(
text/11-fm-why-use-functional-programming.txt:58,搜「silver bullet」)。原文:「没有人会声称 FP 是解决所有软件开发问题的银弹。我总是建议走中间道路;深思熟虑的均衡混合通常是最好的解。」 ↩ ↩2 -
出处:「Why Use Functional Programming?」第 68 段(
text/11-fm-why-use-functional-programming.txt:68,搜「precise definition」)。原文:「对于什么构成函数式语言,不存在精确定义」;本书口径是「支持常见 FP 特性即算」。 ↩ ↩2 -
出处:「Why Use Functional Programming?」第 29 段(
text/11-fm-why-use-functional-programming.txt:29,搜「LISP」)。原文注释:FP 不是现代风潮,仍在常用的第二老语言 LISP(1958)完全建立在 FP 上。 ↩ -
出处:「Why Use Functional Programming?」第 76 段(
text/11-fm-why-use-functional-programming.txt:76,搜「first-class」)。原文:函数是一等对象意味着「存进变量、当参数传、作为返回值」与其他对象无异。 ↩ -
出处:「Why Use Functional Programming?」第 101 段(
text/11-fm-why-use-functional-programming.txt:101,搜「higher-order」)。原文:收函数为参或返回函数的即高阶函数;普通函数称一阶函数。 ↩ -
出处:「Why Use Functional Programming?」第 128 段(
text/11-fm-why-use-functional-programming.txt:128,搜「declarative」)。原文:声明式「指定你想要什么,而不是完成它所需的详细步骤」。 ↩ -
出处:「Why Use Functional Programming?」第 135–159 段(
text/11-fm-why-use-functional-programming.txt:135,搜「filter()」)。原文列出 filter/find/findIndex/some/every/map/reduce/reduceRight 的用途;findLast/findLastIndex见第 206、209 段(搜「findLast」)。 ↩ -
出处:「Why Use Functional Programming?」第 234 段(
text/11-fm-why-use-functional-programming.txt:234,搜「includes()」)。原文注释:找特定值时.includes()、.indexOf()(书中写作 .index())、.lastIndexOf()可用,但「不如 FP 方法灵活,因为它们只能找值,不能测任意条件」。 ↩ -
出处:「Why Use Functional Programming?」第 295 段(
text/11-fm-why-use-functional-programming.txt:295,搜「runtime error」)。原文:不必给.reduce()初值,但更安全的是给;「对空数组归约而不给初值会得到运行时错误」。 ↩ -
出处:「Why Use Functional Programming?」第 172 段(
text/11-fm-why-use-functional-programming.txt:172,搜「irrelevant」)。原文:「即使这些事是真的(数组方法更慢),也无关紧要……写更长、更容易出错的代码没有多大意义。」 ↩ ↩2 -
出处:「Why Use Functional Programming?」第 399 段(
text/11-fm-why-use-functional-programming.txt:399,搜「pure function」)。原文:「当一个函数只依赖它收到的参数且不产生任何副作用时,它被称为纯函数」;与数学函数类比。 ↩ -
出处:「Why Use Functional Programming?」第 402 段(
text/11-fm-why-use-functional-programming.txt:402,搜「random numbers」)。原文:纯函数不能依赖随机数、一天中的时间、I/O 结果等。 ↩ -
出处:「Why Use Functional Programming?」第 413 段(
text/11-fm-why-use-functional-programming.txt:413,搜「previous history」)。原文:调试依赖全局状态的函数更难,因为必须理解「程序状态如何到达」,而这需要理解运行代码的全部先前历史。 ↩ -
出处:「Why Use Functional Programming?」第 423–426 段(
text/11-fm-why-use-functional-programming.txt:423,搜「between calls」)。原文:内部状态会让同样输入的后续调用返回不同输出;并解释函数式程序员不喜欢与对象工作的原因。 ↩ -
出处:「Why Use Functional Programming?」第 436 段(
text/11-fm-why-use-functional-programming.txt:436,搜「by reference」)。原文:对象和数组按引用传参,改参数即改原物;myArray.sort()、pop()、splice()等「by definition」改输入。 ↩ -
出处:「Why Use Functional Programming?」第 439 段(
text/11-fm-why-use-functional-programming.txt:439,搜「toSorted()」)。原文:ECMAScript 近来新增了toSorted()、toReversed()、toSpliced()这些不改原数组的版本。 ↩ -
出处:「Why Use Functional Programming?」第 449 段(
text/11-fm-why-use-functional-programming.txt:449,搜「daily news」)。原文:调 API 拿每日新闻、Math.random()、涉及时钟的函数天然不纯;console.log()也不纯,因为用户会看到控制台输出的变化(第 452 段,搜「console.log()」)。 ↩ -
出处:「Why Use Functional Programming?」第 368 段(
text/11-fm-why-use-functional-programming.txt:368,搜「addLogging」)与第 385 段(搜「sum2」)。原文:addLogging(sum2)(22, 9)依次打印 Entering/Calculating.../Exiting,返回 31。 ↩ -
出处:「Why Use Functional Programming?」第 472 段(
text/11-fm-why-use-functional-programming.txt:472,搜「updated state」)。原文:需要全局状态的函数应把所需元素当参数;需要更新状态时应「产生一个更新后的状态并返回它」,由调用方负责更新。 ↩ -
出处:「Why Use Functional Programming?」第 501 段(
text/11-fm-why-use-functional-programming.txt:501,搜「getData」)与第 513 段(搜「mock」)。原文:定义getData并作为额外参数传入;「被调用的下层函数现在是纯的,测试时可以提供一个 mock 函数」。 ↩ -
出处:「Why Use Functional Programming?」第 280 段(
text/11-fm-why-use-functional-programming.txt:280,搜「chances of introducing bugs」)。原文:用.map()代替循环「代码更清晰……更短,这意味着引入 bug 的机会更少」;且产新数组不改原数组,故为纯函数。 ↩ ↩2 -
出处:「Why Use Functional Programming?」第 307 段(
text/11-fm-why-use-functional-programming.txt:307,搜「reduceRight()」)。原文:可先.reverse()再.reduce(),但「那会造成副作用——数组被就地翻转」。 ↩ -
出处:「Why Use Functional Programming?」第 485 段(
text/11-fm-why-use-functional-programming.txt:485,搜「unit testing」)。原文:注入「提供更灵活的代码、简化单元测试、允许更容易的维护」。 ↩ -
出处:「Chapter 2」第 7 段(
text/65-ch02-chapter-2.txt:7,搜「Math.PI」)。答案原文:circumference 形式上不纯(用了参数之外的 PI),但 PI 是常量、有 const 保护,「可以勉强接受」;「最好的解法是直接用 Math.PI,不用任何外部变量」。 ↩ -
出处:「Chapter 2」第 58 段(
text/65-ch02-chapter-2.txt:58,搜「parseInt()」)。答案原文:.map()给回调传三个参数(元素、下标、整个数组);parseInt把下标当进制:「8 被按三进制解析产生错误……32 被按五进制解析,等于 17」。 ↩ ↩2 -
出处:「Chapter 2」第 67 段(
text/65-ch02-chapter-2.txt:67,搜「negate」)与第 77 段(搜「findIndex」)。答案原文:const negate = (fn) => (...args) => !fn(...args);;.some()可用findIndex(谓词) !== -1重写,.every()用「找不到满足取反谓词的元素」重写。 ↩ -
出处:「Chapter 2」第 38 段(
text/65-ch02-chapter-2.txt:38,搜「addTiming」)。答案原文:Node.js 需const {performance} = require("perf_hooks");;计时函数在成功与异常两条路径上都输出耗时,并重抛原异常。 ↩