跳转至

第 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);  // 163

a 是累加器(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。

纯函数的好处,书里给了两条,都很硬:

  1. 同样输入必得同样输出——结果不依赖任何外部变量,外部变了也不会悄悄变;
  2. 调用它时不用提心吊胆——不用担心哪里的状态被碰坏,注意力只在参数上。

反过来说,纯函数不能依赖这些东西:随机数、当前时间、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. 可带走的

  1. 五个诉求是 FP 的卖点:好懂、好维护、好测试、模块化、可复用——但作者明说 OOP 同样能做到,别当宗教。
  2. 一等公民 = 函数能进变量、能当参数、能当返回值;高阶函数 = 收函数或返回函数的函数。
  3. 谓词是返回布尔的函数;.filter/.find/.some/.every 全是「按谓词扫描」一家子。
  4. .map 一对一变形、.reduce 全组揉成一个值(记得给初值,空数组才不炸)。
  5. 纯函数检查清单:读全局?存内部状态?改参数?——三问都答「否」才算纯。
  6. 对象和数组按引用传参,函数里改它们就是改调用方的原物;sort()/splice() 这类方法本来就地改动。
  7. 包装器模式:addLogging 那种「原函数不动、套一层附加行为」的写法,是调试与计时、记忆化的通用骨架。
  8. 守不住纯度时:状态当参数传进来、新状态当返回值交出去;不纯的动作当函数注进来(注入模式),测试时换假函数。
  9. .map(parseInt) 的坑记住:回调会收到 (元素, 下标, 数组) 三个参数——签名对不上是静默灾难。

  10. 谓词三兄弟的关系: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」)

  1. 出处:「Why Use Functional Programming?」第 190 段(text/11-fm-why-use-functional-programming.txt:190,搜「myArray」)。原文:let myArray = [22, 9, 60, 12, 4, 56];,本章多数例子共用此数组。 ↩

  2. 出处:「Why Use Functional Programming?」第 191 段(text/11-fm-why-use-functional-programming.txt:191,搜「filter(under21)」)。原文:let newArray = myArray.filter(under21); 结果为 [9, 12, 4];并说明「不需要写任何控制循环、初始化输出数组的代码」。 ↩

  3. 出处:「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 是当前元素。 ↩

  4. 出处:「Why Use Functional Programming?」第 41–53 段(text/11-fm-why-use-functional-programming.txt:41,搜「Understandable」)。原文列五个品质:可理解、可维护、可测试、模块化、可复用。 ↩

  5. 出处:「Why Use Functional Programming?」第 58 段(text/11-fm-why-use-functional-programming.txt:58,搜「silver bullet」)。原文:「没有人会声称 FP 是解决所有软件开发问题的银弹。我总是建议走中间道路;深思熟虑的均衡混合通常是最好的解。」 ↩↩

  6. 出处:「Why Use Functional Programming?」第 68 段(text/11-fm-why-use-functional-programming.txt:68,搜「precise definition」)。原文:「对于什么构成函数式语言,不存在精确定义」;本书口径是「支持常见 FP 特性即算」。 ↩↩

  7. 出处:「Why Use Functional Programming?」第 29 段(text/11-fm-why-use-functional-programming.txt:29,搜「LISP」)。原文注释:FP 不是现代风潮,仍在常用的第二老语言 LISP(1958)完全建立在 FP 上。 ↩

  8. 出处:「Why Use Functional Programming?」第 76 段(text/11-fm-why-use-functional-programming.txt:76,搜「first-class」)。原文:函数是一等对象意味着「存进变量、当参数传、作为返回值」与其他对象无异。 ↩

  9. 出处:「Why Use Functional Programming?」第 101 段(text/11-fm-why-use-functional-programming.txt:101,搜「higher-order」)。原文:收函数为参或返回函数的即高阶函数;普通函数称一阶函数。 ↩

  10. 出处:「Why Use Functional Programming?」第 128 段(text/11-fm-why-use-functional-programming.txt:128,搜「declarative」)。原文:声明式「指定你想要什么,而不是完成它所需的详细步骤」。 ↩

  11. 出处:「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」)。 ↩

  12. 出处:「Why Use Functional Programming?」第 234 段(text/11-fm-why-use-functional-programming.txt:234,搜「includes()」)。原文注释:找特定值时 .includes()、.indexOf()(书中写作 .index())、.lastIndexOf() 可用,但「不如 FP 方法灵活,因为它们只能找值,不能测任意条件」。 ↩

  13. 出处:「Why Use Functional Programming?」第 295 段(text/11-fm-why-use-functional-programming.txt:295,搜「runtime error」)。原文:不必给 .reduce() 初值,但更安全的是给;「对空数组归约而不给初值会得到运行时错误」。 ↩

  14. 出处:「Why Use Functional Programming?」第 172 段(text/11-fm-why-use-functional-programming.txt:172,搜「irrelevant」)。原文:「即使这些事是真的(数组方法更慢),也无关紧要……写更长、更容易出错的代码没有多大意义。」 ↩↩

  15. 出处:「Why Use Functional Programming?」第 399 段(text/11-fm-why-use-functional-programming.txt:399,搜「pure function」)。原文:「当一个函数只依赖它收到的参数且不产生任何副作用时,它被称为纯函数」;与数学函数类比。 ↩

  16. 出处:「Why Use Functional Programming?」第 402 段(text/11-fm-why-use-functional-programming.txt:402,搜「random numbers」)。原文:纯函数不能依赖随机数、一天中的时间、I/O 结果等。 ↩

  17. 出处:「Why Use Functional Programming?」第 413 段(text/11-fm-why-use-functional-programming.txt:413,搜「previous history」)。原文:调试依赖全局状态的函数更难,因为必须理解「程序状态如何到达」,而这需要理解运行代码的全部先前历史。 ↩

  18. 出处:「Why Use Functional Programming?」第 423–426 段(text/11-fm-why-use-functional-programming.txt:423,搜「between calls」)。原文:内部状态会让同样输入的后续调用返回不同输出;并解释函数式程序员不喜欢与对象工作的原因。 ↩

  19. 出处:「Why Use Functional Programming?」第 436 段(text/11-fm-why-use-functional-programming.txt:436,搜「by reference」)。原文:对象和数组按引用传参,改参数即改原物;myArray.sort()、pop()、splice() 等「by definition」改输入。 ↩

  20. 出处:「Why Use Functional Programming?」第 439 段(text/11-fm-why-use-functional-programming.txt:439,搜「toSorted()」)。原文:ECMAScript 近来新增了 toSorted()、toReversed()、toSpliced() 这些不改原数组的版本。 ↩

  21. 出处:「Why Use Functional Programming?」第 449 段(text/11-fm-why-use-functional-programming.txt:449,搜「daily news」)。原文:调 API 拿每日新闻、Math.random()、涉及时钟的函数天然不纯;console.log() 也不纯,因为用户会看到控制台输出的变化(第 452 段,搜「console.log()」)。 ↩

  22. 出处:「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。 ↩

  23. 出处:「Why Use Functional Programming?」第 472 段(text/11-fm-why-use-functional-programming.txt:472,搜「updated state」)。原文:需要全局状态的函数应把所需元素当参数;需要更新状态时应「产生一个更新后的状态并返回它」,由调用方负责更新。 ↩

  24. 出处:「Why Use Functional Programming?」第 501 段(text/11-fm-why-use-functional-programming.txt:501,搜「getData」)与第 513 段(搜「mock」)。原文:定义 getData 并作为额外参数传入;「被调用的下层函数现在是纯的,测试时可以提供一个 mock 函数」。 ↩

  25. 出处:「Why Use Functional Programming?」第 280 段(text/11-fm-why-use-functional-programming.txt:280,搜「chances of introducing bugs」)。原文:用 .map() 代替循环「代码更清晰……更短,这意味着引入 bug 的机会更少」;且产新数组不改原数组,故为纯函数。 ↩↩

  26. 出处:「Why Use Functional Programming?」第 307 段(text/11-fm-why-use-functional-programming.txt:307,搜「reduceRight()」)。原文:可先 .reverse() 再 .reduce(),但「那会造成副作用——数组被就地翻转」。 ↩

  27. 出处:「Why Use Functional Programming?」第 485 段(text/11-fm-why-use-functional-programming.txt:485,搜「unit testing」)。原文:注入「提供更灵活的代码、简化单元测试、允许更容易的维护」。 ↩

  28. 出处:「Chapter 2」第 7 段(text/65-ch02-chapter-2.txt:7,搜「Math.PI」)。答案原文:circumference 形式上不纯(用了参数之外的 PI),但 PI 是常量、有 const 保护,「可以勉强接受」;「最好的解法是直接用 Math.PI,不用任何外部变量」。 ↩

  29. 出处:「Chapter 2」第 58 段(text/65-ch02-chapter-2.txt:58,搜「parseInt()」)。答案原文:.map() 给回调传三个参数(元素、下标、整个数组);parseInt 把下标当进制:「8 被按三进制解析产生错误……32 被按五进制解析,等于 17」。 ↩↩

  30. 出处:「Chapter 2」第 67 段(text/65-ch02-chapter-2.txt:67,搜「negate」)与第 77 段(搜「findIndex」)。答案原文:const negate = (fn) => (...args) => !fn(...args);;.some() 可用 findIndex(谓词) !== -1 重写,.every() 用「找不到满足取反谓词的元素」重写。 ↩

  31. 出处:「Chapter 2」第 38 段(text/65-ch02-chapter-2.txt:38,搜「addTiming」)。答案原文:Node.js 需 const {performance} = require("perf_hooks");;计时函数在成功与异常两条路径上都输出耗时,并重抛原异常。 ↩