跳到主要内容

高阶函数 — 把「做什么」当参数传

这一章讲三件事: 为什么「词汇表与问题对应」的程序更可能对; 高阶函数怎么把「动作」抽象出来(filter/map/reduce 三板斧从哪来); 以及组合这些小函数写出的管道长什么样、要付什么代价。 读完你能看懂任何一条 xx.filter(…).map(…).reduce(…) 链在做什么。

1. 先看现象:一行的程序为什么更可能对

导论里留过一个问题,这里正面回答。同一件事(1 加到 10)两种写法:六行的 while 循环,和一行 console.log(sum(range(1, 10)))把 sum 和 range 的定义都算进去,第二种反而更大——但作者断言它更可能没有 Bug,理由是:它用的词汇表与问题对应。求和这件事,本来就不是关于循环和计数器的,是关于范围与和的1

sum/range 的定义内部当然还有循环,但它们各自表达的概念更简单,所以更容易写对2

这类「编程里的词汇表」有个正式名字:抽象(abstraction)——让我们在更高的高度谈问题,不被无趣细节绊住。书里用两份豌豆汤菜谱打比方:第一份逐步教你「拿刀切」,第二份直接写「soak、simmer、chop」——更短,但你得先会那几个词。编程里没人给你现成词典,所以常掉进第一份菜谱的写法;有用的本事是察觉自己正在过低的抽象层上干活3

2. 函数当值:抽象「动作」而不是数据

普通函数能抽象「值」(比如 zeroPad 抽象了补零)。可「把某件事做 N 遍」里的那件事是空着的——repeat(n, action) 把它做成参数:

function repeat(n, action) {
for (let i = 0; i < n; i++) action(i);
}
repeat(5, i => labels.push(`Unit ${i + 1}`));
// labels → ["Unit 1", …, "Unit 5"]

「做事」用函数值表示,闭包(03 章)让这个函数能就地现造4

以函数为参数、或以函数为返回值的函数,叫高阶函数(higher-order function)。这个词来自数学,数学里函数和其他值的分界更受尊重;在 JS 里这只是「函数是普通值」的自然推论5。它有三种典型形态:

形态书里的例子它抽象了什么
造出函数greaterThan(10) 返回 m => m > 10「与某个阈值(一条及格线)比较」里的阈值
改造函数noisy(Math.min) 包一层运行记录再原样放行「调用前后各记一笔」
造控制流unless(test, then) = 反着写的 if「不成立才做」6

标准库里最常见的入门款是 forEach:数组自带的「把 for-of 换成函数调用」——["A", "B"].forEach(l => console.log(l))7

3. 一份真实数据:SCRIPTS

空谈无益,书里搬来一份数据:Unicode 定义的 140 种书写文字,81 种仍活着,59 种已成历史。每条记录长这样8:

{
name: "Coptic",
ranges: [[994, 1008], …], // 该文字占用的码点区间,含头不含尾
direction: "ltr", // 还有 rtl(右到左)、ttb(上到下)
year: -200, living: false, link: …
}

下面三个方法都为回答「关于这份数据的某个问题」而生。

4. 三板斧:filter、map、reduce

filter——筛。 解决的问题:留下数组里「通过测试」的那些。做法:逐个喂给测试函数,通过的进新数组。它建新数组、不动旧的,所以是纯函数9

map——变形。 解决的问题:结构不变、内容要换。对每个元素调一次函数,收集返回值;新数组与旧数组等长rtlScripts.map(s => s.name) 把对象数组换成名字数组10

reduce——折叠。 解决的问题:把整个数组压成一个值。带一个「组合函数」和一个起始值,从起点开始逐个合并11:

function reduce(array, combine, start) {
let current = start;
for (let element of array) current = combine(current, element);
return current;
}
reduce([1, 2, 3, 4], (a, b) => a + b, 0); // → 10

标准版 reduce 有个便利:数组非空时可以省起始值,拿第一个元素当起点12

三板斧叠起来就能干正经事:哪种文字字符最多? 先写 characterCount(对 ranges 做一次 reduce 求区间长度和),再对整个数据集做一次 reduce,两两比较留下大的——答案是 Han:九万多个字符,一骑绝尘13。顺带一个书里的花边:中日韩共用这批汉字码位是当年(美国主导的)Unicode 联盟为省码位拍板,这个决定「至今让一些人大怒」14

5. 主走查:textScripts — 一条管道的全程

问题:给一段混合文字,报告每种文字各占百分之几。输入就用书里的原句:「英国的狗说 "woof", 俄罗斯的狗说 "тяв"」,输出:61% Han, 22% Latin, 17% Cyrillic15

管道由三个零件接成,我们带真数字走一遍:

输入串(按字符逐个过)

① characterScript(码点)
对每个字符查它在哪个区间 → 得到所属文字名
「英」「国」→ Han;"w""o""o""f" → Latin;"т""я""в" → Cyrillic
专名/标点/空格 → 查不到,记作 "none"

② countBy(按名字分组计数)
得 [ {name: "Han", count: 7}, {name: "Latin", count: …},
{name: "Cyrillic", count: …}, {name: "none", count: …} ]
countBy 内部用 find 在已知分组里找同名项:有就 count++,没有就 push 新组

③ filter 剔掉 "none" → reduce 求总数 → map 变成百分比字符串 → join
总数按书里结果倒推约 31 个有归属的字符
Han 7/31 ≈ 61%(书里的中文例句汉字占七成上下,量级一致)
输出 "61% Han, 22% Latin, 17% Cyrillic"

图说:①②里的百分比字符数为演示口径,最终百分比与书一致。

看每个零件在这条管道上的位置:characterScript 是逐字符的分类器(给每个字符判归属);countBy 是通用分组器(它自己用了 find——「返回第一个通过测试的元素,找不到给 undefined」);filter 剔除杂讯;reduce 出总数;map 做最终变形;join 收尾16

6. 组合的代价:抽象不是免费的

同样的统计,不用高阶函数也能写——一个大循环、两个累加变量。作者并不否认循环版可行(找最大文字的循环版只多 4 行)17;他给的是两条诚实的对照:

  1. 管道版真的会造中间数组:filter 建一个、map 再建一个;大循环版只动几个数。处理巨量数据、反复处理时,这个差距值得在乎;平时读得懂优先18;
  2. 管道版每个中间结果都是一个完整的值,所以 average 能单独抽出来复用;大循环版把「求和」「计数」「筛选」揉在一锅里,想抽都没处下刀。书里的活例:活着的文字平均起源年 1165,死文字 204——一行管道,一眼看懂「先筛活的、取年份、求平均、四舍五入」19

7. 一笔旧账:UTF-16 为什么让 length 不可信

管道的输入若含 emoji,第 ② 步就会撞上老坑。复习 02 章的伏笔:JS 字符串是 16 位「码元」(code unit)的序列;早年以为 6.5 万个号够用,发现不够后发明了 UTF-16——常用字符占一个码元,剩下的占一对20

书里对 UTF-16 的评价毫不客气:「如今被普遍认为是坏主意」「几乎像故意招引错误」;它唯一的「功劳」是 emoji 普及后人人都要处理双码元字符,坑的负担总算公平分摊了21。具体症状:

"🐴👟".length // → 4 两个 emoji,四个码元
"🐴👟".charCodeAt(0) // → 55357 半个马的码元
"🐴👟".codePointAt(0)// → 128052 完整的「马」码点

应对:codePointAt 给完整码点(但下标仍是码元序号);for-of 遍历字符串直接给你真的字符——本章的 textScripts 正是用 for-of 来逐字符过河的22

8. 作者的判断与证据

  • 「词汇对应问题 → 更可能对」:论证性主张,依据是 sum/range 内部更简单的直觉;
  • 「Han 统一让一些人大怒」:书里明确说明这是情绪事实而非技术错误;
  • 「抽象优先、必要时换速度」:给出了具体分界(巨量+反复),不是空口号18;
  • 「UTF-16 是坏主意」:行业评价,书里以「很多人这么认为」的口吻呈现21

判断(我们的,不是书里的): 本章真正的武器不是 filter/map/reduce 这三个名字, 而是「写一个带空洞的函数,让调用者用函数值填洞」这个手法。第 07 章的机器人接受「策略函数」、 第 13 章的显示层接受「画图函数」,都是同一手法在更大尺度上的复用。 如果错,会错在: 如果某生态的主流抽象不是回调而是数据(如声明式查询), 「带空洞的函数」就不是该场景的首选——手法有边界,不是万能钥匙。

9. 边界与局限

  • sortevery(some 的反面,书里形容成「数组的 && 与 ||」)等只在练习出现23;
  • 管道的性能账只在「巨量+反复」前提下翻盘,书没给具体阈值;
  • Unicode 的属性类(\p{L} 等)留给第 09 章;本章只用「区间表」这种朴素表示;
  • 书里没提流式处理大数据的替代方案(那要到 Node 的流,第 14 章)。

10. 可带走的

  1. 词汇表与问题对应的程序更可能对;察觉自己在过低抽象层干活是本事;
  2. 高阶函数=以函数为参数或返回值;它抽象的是动作;
  3. filter 筛(纯、建新数组)、map 变形(等长)、reduce 折叠(组合函数+起始值);
  4. 标准版 reduce 可省起始值,拿首元素垫底;
  5. 主走查管道:分类 → 分组计数 → 剔杂 → 求和 → 变形 → 拼接;
  6. 中间结果是值,所以能抽出 average;中间结果是数组,所以大数据会变慢——通常仍值得;
  7. 码元≠字符:"🐴👟".length 是 4;要字符用 for-of,要码点用 codePointAt;
  8. countBy 的「分组计数」是数据分析里出场率最高的半成品;
  9. 阈值比较、运行记录包装、反向 if——「造函数/改函数/造控制流」三形态认得即可;
  10. 拿不准先 reduce;reduce 是三板斧里唯一「变维」的。

11. 原文地图

主题原书章原文位置
sum(range) 为什么更可能对Higher-Order Functionstext/08-fm-higher-order-functions.txt:26(搜「more likely to be correct」) · :28(搜「vocabulary」)
豌豆汤菜谱与抽象同上text/08-fm-higher-order-functions.txt:36(搜「pea soup」) · :50(搜「too low a level」)
repeat 与传入动作同上text/08-fm-higher-order-functions.txt:72(搜「repeat」) · :86(搜「Unit」)
高阶函数定义同上text/08-fm-higher-order-functions.txt:96(搜「higher-order functions」)
造函数/改函数/控制流同上text/08-fm-higher-order-functions.txt:100(搜「greaterThan」) · :109(搜「noisy」) · :123(搜「unless」)
forEach同上text/08-fm-higher-order-functions.txt:137(搜「forEach」)
SCRIPTS 数据集 140/81/59同上text/08-fm-higher-order-functions.txt:145(搜「140」)
Coptic 记录与区间同上text/08-fm-higher-order-functions.txt:152(搜「Coptic」) · :162(搜「inclusive」)
filter 纯函数同上text/08-fm-higher-order-functions.txt:168(搜「filter」) · :183(搜「pure」)
map 等长变形同上text/08-fm-higher-order-functions.txt:194(搜「same length」)
reduce 与省略起点同上text/08-fm-higher-order-functions.txt:214(搜「fold」) · :229(搜「first element」)
characterCount 与 Han同上text/08-fm-higher-order-functions.txt:236(搜「characterCount」) · :249(搜「89,000」)
Han unification 的怒火同上text/08-fm-higher-order-functions.txt:249(搜「very angry」)
循环版对比同上text/08-fm-higher-order-functions.txt:255(搜「biggest」)
活/死文字平均年 1165/204同上text/08-fm-higher-order-functions.txt:275(搜「1165」) · :278(搜「204」)
管道读法同上text/08-fm-higher-order-functions.txt:280(搜「pipeline」)
中间数组代价同上text/08-fm-higher-order-functions.txt:296(搜「less work」)
UTF-16 批判同上text/08-fm-higher-order-functions.txt:322(搜「code units」) · :324(搜「bad idea」)
🐴👟 的 length=4同上text/08-fm-higher-order-functions.txt:329(搜「horseShoe」)
codePointAt 与 for-of同上text/08-fm-higher-order-functions.txt:339(搜「codePointAt」) · :341(搜「for/of」)
countBy 与 find同上text/08-fm-higher-order-functions.txt:356(搜「countBy」) · :375(搜「find」)
textScripts 与中文例句同上text/08-fm-higher-order-functions.txt:379(搜「textScripts」) · :393(搜「woof」)
every/some 类比同上text/08-fm-higher-order-functions.txt:420(搜「every」)

Footnotes

  1. 出处:「Higher-Order Functions」第 26 段(text/08-fm-higher-order-functions.txt:26,搜「more likely to be correct」)。

  2. 出处:「Higher-Order Functions」第 30 段(text/08-fm-higher-order-functions.txt:30,搜「simpler concepts」)。

  3. 出处:「Higher-Order Functions」第 34 段(text/08-fm-higher-order-functions.txt:34,搜「abstractions」)与第 50 段(text/08-fm-higher-order-functions.txt:50,搜「too low a level」)。

  4. 出处:「Higher-Order Functions」第 70 段(text/08-fm-higher-order-functions.txt:70,搜「pass our action」)与第 83 段(text/08-fm-higher-order-functions.txt:83,搜「on the spot」)。

  5. 出处:「Higher-Order Functions」第 96 段(text/08-fm-higher-order-functions.txt:96,搜「higher-order functions」)。

  6. 出处:「Higher-Order Functions」第 100 段(text/08-fm-higher-order-functions.txt:100,搜「greaterThan」)、第 109 段(text/08-fm-higher-order-functions.txt:109,搜「noisy」)与第 123 段(text/08-fm-higher-order-functions.txt:123,搜「unless」)。

  7. 出处:「Higher-Order Functions」第 135 段(text/08-fm-higher-order-functions.txt:135,搜「forEach」)。

  8. 出处:「Higher-Order Functions」第 145 段(text/08-fm-higher-order-functions.txt:145,搜「140 different scripts」)、第 152 段(text/08-fm-higher-order-functions.txt:152,搜「Coptic」)与第 162 段(text/08-fm-higher-order-functions.txt:162,搜「noninclusive」)。

  9. 出处:「Higher-Order Functions」第 168 段(text/08-fm-higher-order-functions.txt:168,搜「test」)与第 183 段(text/08-fm-higher-order-functions.txt:183,搜「pure」)。

  10. 出处:「Higher-Order Functions」第 194 段(text/08-fm-higher-order-functions.txt:194,搜「same length」)。

  11. 出处:「Higher-Order Functions」第 214 段(text/08-fm-higher-order-functions.txt:214,搜「fold」)与第 218 段(text/08-fm-higher-order-functions.txt:218,搜「reduce」)。

  12. 出处:「Higher-Order Functions」第 229 段(text/08-fm-higher-order-functions.txt:229,搜「first element」)。

  13. 出处:「Higher-Order Functions」第 236 段(text/08-fm-higher-order-functions.txt:236,搜「characterCount」)与第 249 段(text/08-fm-higher-order-functions.txt:249,搜「89,000」)。

  14. 出处:「Higher-Order Functions」第 249 段(text/08-fm-higher-order-functions.txt:249,搜「very angry」)。

  15. 出处:「Higher-Order Functions」第 379 段(text/08-fm-higher-order-functions.txt:379,搜「textScripts」)与第 394 段(text/08-fm-higher-order-functions.txt:394,搜「61% Han」)。

  16. 出处:「Higher-Order Functions」第 356 段(text/08-fm-higher-order-functions.txt:356,搜「countBy」)、第 375 段(text/08-fm-higher-order-functions.txt:375,搜「find」)、第 304 段(text/08-fm-higher-order-functions.txt:304,搜「characterScript」)、第 164 段(text/08-fm-higher-order-functions.txt:164,搜「filter」)。

  17. 出处:「Higher-Order Functions」第 255 段(text/08-fm-higher-order-functions.txt:255,搜「biggest」)与第 265 段(text/08-fm-higher-order-functions.txt:265,搜「four lines longer」)。

  18. 出处:「Higher-Order Functions」第 296 段(text/08-fm-higher-order-functions.txt:296,搜「less work」)。 2

  19. 出处:「Higher-Order Functions」第 275 段(text/08-fm-higher-order-functions.txt:275,搜「1165」)、第 278 段(text/08-fm-higher-order-functions.txt:278,搜「204」)与第 280 段(text/08-fm-higher-order-functions.txt:280,搜「pipeline」)。

  20. 出处:「Higher-Order Functions」第 322 段(text/08-fm-higher-order-functions.txt:322,搜「code units」)。

  21. 出处:「Higher-Order Functions」第 324 段(text/08-fm-higher-order-functions.txt:324,搜「bad idea」)。 2

  22. 出处:「Higher-Order Functions」第 339 段(text/08-fm-higher-order-functions.txt:339,搜「codePointAt」)与第 341 段(text/08-fm-higher-order-functions.txt:341,搜「real characters」)。

  23. 出处:「Higher-Order Functions」第 420 段(text/08-fm-higher-order-functions.txt:420,搜「every」)。