跳到主要内容

程序的形状 — 语句、绑定与函数

这一章讲三件事: 程序怎么从「产生值」升级成「做事」(语句、副作用); 程序怎么记住东西、怎么重复、怎么选择(绑定、循环、分支); 以及 JavaScript 最核心的一步——把一段程序本身装进值里(函数),由此长出作用域、调用栈与闭包。 原书这两章是全程的主干,我们合并成一章讲。

1. 先看现象:不产生任何东西的程序

1;
!false;

这是一个合法的程序。运行它:什么都不会发生1

要理解为什么,得先分清两种积木:

  • 表达式(expression): 产生值的代码片段。22 是,"psychoanalysis" 是,括号包起来的、运算符连起来的也是。表达式能像从句一样层层嵌套,所以能描述任意复杂的计算2;
  • 语句(statement): 完整的句子;程序就是一份语句清单3

上面那个「什么都不会发生」的程序,是两条语句,各自算了 1true,然后随手扔掉。语句必须对世界留下痕迹——显示点什么,或改变机器的状态——这些痕迹就叫副作用(side effect);不留痕迹的语句没有存在意义4

顺带一条实用规矩:语句末尾的分号,什么时候能省「规则复杂且容易出错」,所以作者全书一律写分号,也建议你这么办5

2. 绑定:抓住值

新造的值要么马上用掉,要么消散。想抓住它,用绑定(binding),也就是变量(variable):

let caught = 5 * 5; // caught 抓住了 25

let 是关键字,宣布「这句要定义一个绑定」;= 把右边表达式的值递给它。之后名字就能当表达式用:let ten = 10; ten * ten → 1006

书里给了一个全书最好的比喻:绑定是触手,不是盒子。 它不装值,只抓值;两个绑定可以抓同一个值;要记新东西,就长一条新触手,或把旧触手挪过去抓7

let luigisDebt = 140; // Luigi 欠你 140
luigisDebt = luigisDebt - 35; // 还了 35,触手改抓 105
// → 105

图说:赋值不是「修改数字」,是松开旧的、抓住新的。

定义时不给值,触手抓了个空——问它要值,得 undefined8

三个关键字两张面孔:let 是现代标准写法;const 定义终生不改指向的常量绑定;var 是 2015 年前的旧写法,行为古怪(见第 5 节),书里基本不用9

某时刻所有绑定及其值的集合,叫做环境(environment)。程序启动时环境不是空的——语言标准自带一批(console、Math、prompt……)10

3. 控制流:分岔与回头

程序默认从上到下一句句执行。想不走直线,靠两类结构,合称控制流(control flow,程序执行顺序的走向)11

分岔: if 按布尔表达式决定执行还是跳过;else 给出另一条路;else if 串起来就是多路分发——先查 num < 10 打 Small,不中再查 < 100 打 Medium,都不中打 Large12

回头: 打印 0 到 12 的偶数,总不能写七行 console.log。while 让程序回到之前的某点,带着当前状态再跑一遍:

let number = 0;
while (number <= 12) {
console.log(number); // 0, 2, 4, …, 12
number = number + 2; // 绑定在这里「记进度」
}

binding 在循环里扮演进度条:每圈加 2,每圈开头跟 12 比一次。同一个结构换成算 2 的 10 次方:result 依次 1→2→4→8→16→32→64→128→256→512→1024,counter 从 0 数到 10,输出 102413。作者还自嘲:其实有现成的 2 ** 10,「但那会毁了教学例子」14

因为「初始化计数器→测条件→更新计数器」这个三件套太常用,for 循环把它归拢进一行括号,三段用分号隔开,程序一模一样但状态一目了然15

两个中途变道的关键字:break 立刻跳出整个循环(书里用它找「大于等于 20 的第一个 7 的倍数」——20 试到 21,21 % 7 == 0,输出 21 就跑);continue 跳过本轮剩下的部分直接进下一圈16。漏写 break 或条件永远为真,程序就进无限循环,再也不结束17

最后是 switch:为「按值分发」而生,但语法继承自 C/Java 一系,忘了写 break 会「贯穿」到下一个 case 继续执行;作者直言一串 if/else 有时反而更好看18

4. 函数:把一段程序包进值里

函数(function)= 包在值里的一段程序。定义一个,就是造一个指向它的绑定:

const square = function(x) {
return x * x;
};
square(12); // → 144

括号里的 x参数(parameter):它表现得像普通绑定,只是初值由调用者递进来return 立刻跳出函数并交出值;没有 return 的函数交 undefined19

三种写法,一个东西:

写法例子备注
函数表达式const f = function(a) {…}最基础
函数声明function g(a, b) {…}概念上被搬到作用域顶部,先调用后定义也行20
箭头函数let h = a => a % 3;2015 年加入,为了少打字;单参数可省括号,单表达式体隐式 return21

关于箭头函数,书里有句很松弛的话:「语言里同时有箭头函数和函数表达式,并没有什么深层原因」——选哪个基本是口味问题22

参数方面 JS 极度宽容:多传的被忽略(square(4, true, "hedgehog") 照样得 16),少传的补 undefined。坏处是传错数量没有任何人提醒你;好处是可以设计「一参两用」的函数,还能给参数配默认值:function roundTo(n, step = 1) 不传 step 就按 1 算23

5. 作用域:谁看得见谁

每个绑定都有作用域(scope):程序里「这个名字可见」的那一段24

  • 函数外定义的绑定是全局的,哪里都可见;
  • 函数参数和函数内声明的绑定是局部的,每次调用都重新造一批——各次调用互不干扰,每个函数调用(唤起一次函数)都活在自己的小世界里25;
  • let/const 其实是块级的:if 或循环体里造的绑定,块外看不见。var 不一样,它无视块、在整个函数(或全局)可见——这正是它「行为古怪」的地方26;
  • 内外同名时,内层遮住外层:halve 函数里的 n 不是外面那个 n(调用 halve(100) 后,外面的 n = 10 纹丝不动)27;
  • 块与函数可以层层嵌套,内层能看到所有外层。这套「可见性由程序文本里的位置决定」的规则,叫词法作用域(lexical scoping)——看代码长在哪,就知道它能看见谁28

6. 调用栈:计算机怎么记得「等会回哪」

函数会跳走,但跳走前必须记得从哪来,返回时好回去。这份「来路」存在**调用栈(call stack)**上:每次调用把当前上下文(此刻的来路)压到栈顶,返回时弹掉栈顶接着跑29

栈要占内存。递归太深、或互相调用不落地,栈就会撑爆——书里的演示是让鸡和蛋互相「解释来源」:

function chicken() { return egg(); }
function egg() { return chicken(); }
chicken()

它不是无限运行,而是很快以 "out of stack space" 或 "too much recursion" 报错收场30

7. 主走查:闭包 — 函数记得它出生的地方

现在到本章最要紧的机制。局部绑定在函数返回后不是该销毁吗?看这段(书里的原例):

function wrapValue(n) {
let local = n;
return () => local;
}
let wrap1 = wrapValue(1);
let wrap2 = wrapValue(2);
wrap1(); // → 1
wrap2(); // → 2

wrapValue(1) 早就返回了,local 居然还活着,而且 wrap1 抓着 1、wrap2 抓着 2,互不串门。这种「函数引用了外围作用域里特定一份局部绑定」的现象,叫闭包(closure);这样的函数就叫一个闭包31

把「造函数的函数」推进一步,就是本章的主走查——multiplier 工厂:

function multiplier(factor) {
return number => number * factor;
}
let twice = multiplier(2);
twice(5); // → 10

逐帧看每个时刻的环境状态:

① 调用 multiplier(2)
造出环境 A:{ factor: 2 }
② 它返回箭头函数 number => number * factor
——这个函数值里装着两样东西:代码 + 环境A 的引用
③ 环境A 的其他部分按理该销毁了
但这个函数值还抓着它,于是 { factor: 2 } 活了下来
④ twice = 那个函数值
⑤ 调用 twice(5)
造出环境 B:{ number: 5 }(参数自己的环境)
计算时看到 factor —— 去出生环境A 里找到 2
5 * 2 → 10
⑥ 再造一个 triple = multiplier(3)
又有一个新的 { factor: 3 },与 ④ 那份完全无关

书里给的心智图像一句话:函数值 = 代码体 + 它被创建时的环境;被调用时,它看到的是出生环境,不是调用环境32

这次走查顺手解释了两件前面埋的事:第 4 节说「每次调用重新造一批局部绑定」——正因如此,同一个 multiplier 造出的每个函数才各抓各的 factor;第 5 节的词法作用域——箭头函数去哪找 factor,是写代码的位置决定的,不是调用位置。

8. 递归:自己调用自己,以及一次「速度 vs 优雅」的裁决

函数调用自己没问题,只要别把栈撑爆。用递归写幂运算,几乎就是数学定义的直译:

function power(base, exponent) {
if (exponent == 0) return 1;
return base * power(base, exponent - 1);
}
power(2, 3); // → 8(2*2*2)

但作者马上给出代价:在典型实现里,它比 for 循环版慢约 3 倍——跑一圈循环比来回调函数便宜33

由此引出一组判断,值得整段留下:速度与优雅是一条连续谱,一端对机器友好,一端对人友好;几乎任何程序都能靠写得更绕来提速;多数代码根本跑不了几万次,不值得优化——所以先写正确且易懂的,真担心慢,先测量再说34。这是本章最重要的可迁移判断。

不过有些问题天然是递归的:要探索层层分叉的分支。书里的谜题:从 1 出发,每一步可以 +5 或 ×3,怎么凑出目标数?(13 可以:×3 再 +5 加两次;15 不行。)35

find(1, "1")
find(6, "(1 + 5)")
find(11, "((1 + 5) + 5)")
find(16, "…+5)") → 太大,此路不通
find(33, "…*3)") → 太大,此路不通
find(18, "((1 + 5) * 3)") → 太大
find(3, "(1 * 3)")
find(8, "((1 * 3) + 5)")
find(13, "(((1 * 3) + 5) + 5)") → 找到!

缩进就是调用栈的深度。find 的规则只有三条:等于目标,交出「历史」;超过目标,交 null(加和乘只会更大,这条分支没戏);还没到,就把两条岔路各试一遍,先试 +5,不行(结果是 null)再试 ×3。对 13 的答案是 (((1 * 3) + 5) + 5)。注意书里点明:它不保证找到最短序列,找到一条就收工36

9. 函数怎么长出来:农场库存的三次重构

书里用一个小案例示范「函数从哪来」,全程带具体输出,值得完整留下。

需求:打印农场动物数,数字前补零到三位——007 Cows011 Chickens

第一版:对每种动物复制一遍「转字符串、不够三位就往前补 0」的循环。能用。然后农场主来电:还养了猪。正要再复制粘贴一遍时停手——重复代码是错误的藏身之处37

第二版:把重复部分整体抽出,连打印带标签一起:printZeroPaddedWithLabel(number, label)。能用,但作者不满意:这个名字把三件事(打印、补零、加标签)搅在一起——名字难起,正是概念没切干净的信号38

第三版:只抽出一个概念:

function zeroPad(number, width) {
let string = String(number);
while (string.length < width) string = "0" + string;
return string;
}

打印留在外面。zeroPad 名字一目了然,而且用处一下子变多(把表格的数排整齐时都用得上)39

两条配套判断:一是不确定需要就别加聪明,抵制「给每个功能造框架」的冲动——你会忙于写永远用不上的代码40;二是把函数分成「为副作用而调」与「为返回值而调」,产出值的函数更容易组合——zeroPad 比它的前身有用,不是偶然41

纯函数(pure function) 是后一类的极致:不产生副作用、也不依赖别人的副作用(不读会变的全局绑定);同参数必同值;调用处可以直接换成它的返回值;测一处等于处处可信42。但书里同样明说:不必为不纯羞愧——console.log 就没有纯版本,可它很好用43

10. 作者的判断与证据

  • 「先写对,再测量,再优化」——有依据(递归慢 3 倍是可测的),结论是作者的经验法则33;
  • 「闭包把绑定的生命周期问题整个解放了」——机制层面的描述,本书代码可复验31;
  • 「函数名难起=概念没长好」——作者的实践经验,无统计支撑;
  • 「不要给每种可能都写防御,不确定需要就不加聪明」——同上,立场而非定律40

判断(我们的,不是书里的): 这一章其实是全书的技术分水岭。01-02 章是「计算机的世界」, 从本章起是「程序员的工程」:作用域、栈、重构、测试成本——后面所有章(模块、错误、异步、状态管理) 都建立在这一章的词上。如果只用一章的时间换理解,应该换这一章。 如果错,会错在: 如果读者的目标只是「能改改网页脚本」,第 12-13 章的 DOM 与事件反而更贴身——分水岭的排位取决于用途。

11. 边界与局限

  • this 与对象方法、箭头函数在 this 上的特殊行为,本章刻意没讲(要等对象章);
  • 生成器与 async(能暂停的函数)在第 11 章才出现——那里会告诉你 async 函数其实是一种生成器;
  • 书里讲的调用栈是一幅教学用的简化图像,不同引擎的实现细节(尾调用等)没提;
  • 「递归慢约 3 倍」是当时的典型实现的量级,不是语言保证。

12. 可带走的

  1. 表达式产值,语句做事;不留副作用的语句等于没写;
  2. 绑定是触手不是盒子;赋值=改抓新值;两个绑定可抓同一个值;
  3. const 锁指向不锁内容(内容能不能变,第 04 章讲);
  4. 循环三件套(初始/条件/更新)用 for 归拢;break 跳出,continue 跳圈;
  5. switch 漏 break 会贯穿——不如 if 链稳妥;
  6. 函数=包在值里的程序;参数初值由调用者给;传多传少都不报错;
  7. 词法作用域:可见性看代码写在哪儿;let/const 块级,var 函数级;
  8. 调用栈记「回哪去」;撑爆它 = "too much recursion";
  9. 闭包 = 代码 + 出生环境;multiplier(2) 造 twice 的六帧走查值得背下来;
  10. 递归适合分支探索,慢约 3 倍;先写对再测量;纯函数好测,但 console.log 无纯版,别有洁癖。

13. 原文地图

主题原书章原文位置
无用程序与副作用Program Structuretext/05-fm-program-structure.txt:23(搜「1;」) · :26(搜「side effects」)
表达式/语句/嵌套Program Structuretext/05-fm-program-structure.txt:15(搜「expression」) · :17(搜「subsentences」)
分号建议Program Structuretext/05-fm-program-structure.txt:28(搜「error prone」)
绑定与触手比喻Program Structuretext/05-fm-program-structure.txt:32(搜「binding, or variable」) · :55(搜「tentacles」)
luigisDebt 140→105Program Structuretext/05-fm-program-structure.txt:59(搜「luigisDebt」)
var/constProgram Structuretext/05-fm-program-structure.txt:79(搜「pre-2015」) · :81(搜「constant」)
环境Program Structuretext/05-fm-program-structure.txt:99(搜「environment」)
if/else/链式条件Program Structuretext/05-fm-program-structure.txt:157(搜「executes or skips」) · :188(搜「at least 10 but below 100」)
while 与 2^10=1024Program Structuretext/05-fm-program-structure.txt:204(搜「even numbers」) · :230(搜「1024」)
2**10 会毁了例子Program Structuretext/05-fm-program-structure.txt:234(搜「ruined」)
for 循环三件套Program Structuretext/05-fm-program-structure.txt:276(搜「two semicolons」)
break 找 21、无限循环Program Structuretext/05-fm-program-structure.txt:297(搜「21」) · :303(搜「infinite loop」)
switch 贯穿Program Structuretext/05-fm-program-structure.txt:352(搜「even across other labels」)
函数定义与 returnFunctionstext/06-fm-functions.txt:19(搜「const square」) · :45(搜「return statement」)
参数由调用者给值Functionstext/06-fm-functions.txt:47(搜「caller」)
声明式写法先调用后定义Functionstext/06-fm-functions.txt:134(搜「moved to the top」)
箭头函数、没有深层原因Functionstext/06-fm-functions.txt:158(搜「no deep reason」)
作用域与隔离Functionstext/06-fm-functions.txt:51(搜「scope」) · :53(搜「own little world」)
var 函数级Functionstext/06-fm-functions.txt:55(搜「pre-2015 JavaScript」)
内层遮蔽 halveFunctionstext/06-fm-functions.txt:69(搜「let n = 10」)
词法作用域Functionstext/06-fm-functions.txt:99(搜「lexical scoping」)
调用栈与爆栈Functionstext/06-fm-functions.txt:184(搜「call stack」) · :186(搜「too much recursion」)
参数宽容、默认参数Functionstext/06-fm-functions.txt:207(搜「broad-minded」) · :223(搜「step = 1」)
wrapValue 与闭包定义Functionstext/06-fm-functions.txt:244(搜「wrapValue」) · :258(搜「closure」)
multiplier(2) 造 twiceFunctionstext/06-fm-functions.txt:266(搜「twice」)
代码+出生环境的心智模型Functionstext/06-fm-functions.txt:272(搜「mental model」)
递归慢 3 倍、速度 vs 优雅Functionstext/06-fm-functions.txt:293(搜「three times slower」) · :295(搜「human-friendliness」)
先测量再优化Functionstext/06-fm-functions.txt:301(搜「measure」)
findSolution(13) 走查Functionstext/06-fm-functions.txt:336(搜「find(1」) · :350(搜「depth of the call stack」)
不保证最短Functionstext/06-fm-functions.txt:326(搜「shortest」)
农场库存 007 CowsFunctionstext/06-fm-functions.txt:364(搜「007 Cows」)
名字搅三件事Functionstext/06-fm-functions.txt:405(搜「conflates three」)
zeroPadFunctionstext/06-fm-functions.txt:409(搜「zeroPad」)
别加聪明、别造框架Functionstext/06-fm-functions.txt:429(搜「refrain from adding cleverness」)
纯函数Functionstext/06-fm-functions.txt:437(搜「pure function」) · :439(搜「pure version of console」)

Footnotes

  1. 出处:「Program Structure」第 26 段(text/05-fm-program-structure.txt:26,搜「useless program」)。

  2. 出处:「Program Structure」第 15 段(text/05-fm-program-structure.txt:15,搜「expression」)与第 17 段(text/05-fm-program-structure.txt:17,搜「subsentences」)。

  3. 出处:「Program Structure」第 19 段(text/05-fm-program-structure.txt:19,搜「list of statements」)。

  4. 出处:「Program Structure」第 26 段(text/05-fm-program-structure.txt:26,搜「side effects」)。

  5. 出处:「Program Structure」第 28 段(text/05-fm-program-structure.txt:28,搜「error prone」)。

  6. 出处:「Program Structure」第 32 段(text/05-fm-program-structure.txt:32,搜「binding, or variable」)与第 36 段(text/05-fm-program-structure.txt:36,搜「keyword」)。

  7. 出处:「Program Structure」第 55 段(text/05-fm-program-structure.txt:55,搜「tentacles」)。

  8. 出处:「Program Structure」第 64 段(text/05-fm-program-structure.txt:64,搜「thin air」)。

  9. 出处:「Program Structure」第 79 段(text/05-fm-program-structure.txt:79,搜「pre-2015」)与第 81 段(text/05-fm-program-structure.txt:81,搜「constant」)。

  10. 出处:「Program Structure」第 99 段(text/05-fm-program-structure.txt:99,搜「environment」)。

  11. 出处:「Program Structure」第 133 段(text/05-fm-program-structure.txt:133,搜「top to bottom」)。

  12. 出处:「Program Structure」第 176 段(text/05-fm-program-structure.txt:176,搜「chain」)与第 188 段(text/05-fm-program-structure.txt:188,搜「below 100」)。

  13. 出处:「Program Structure」第 217 段(text/05-fm-program-structure.txt:217,搜「as long as」)与第 221 段(text/05-fm-program-structure.txt:221,搜「210」)。

  14. 出处:「Program Structure」第 234 段(text/05-fm-program-structure.txt:234,搜「ruined」)。

  15. 出处:「Program Structure」第 265 段(text/05-fm-program-structure.txt:265,搜「slightly shorter」)与第 276 段(text/05-fm-program-structure.txt:276,搜「two semicolons」)。

  16. 出处:「Program Structure」第 289 段(text/05-fm-program-structure.txt:289,搜「divisible by 7」)与第 305 段(text/05-fm-program-structure.txt:305,搜「continue」)。示例输出 21 见第 297 段。

  17. 出处:「Program Structure」第 303 段(text/05-fm-program-structure.txt:303,搜「infinite loop」)。

  18. 出处:「Program Structure」第 336 段(text/05-fm-program-structure.txt:336,搜「somewhat awkward」)与第 352 段(text/05-fm-program-structure.txt:352,搜「even across other labels」)。

  19. 出处:「Functions」第 26 段(text/06-fm-functions.txt:26,搜「parameters」)与第 45 段(text/06-fm-functions.txt:45,搜「return statement」)。

  20. 出处:「Functions」第 134 段(text/06-fm-functions.txt:134,搜「moved to the top」)。

  21. 出处:「Functions」第 158 段(text/06-fm-functions.txt:158,搜「less verbose」)与第 147 段(text/06-fm-functions.txt:147,搜「omit the parentheses」)。

  22. 出处:「Functions」第 158 段(text/06-fm-functions.txt:158,搜「no deep reason」)。

  23. 出处:「Functions」第 202 段(text/06-fm-functions.txt:202,搜「hedgehog」)、第 209 段(text/06-fm-functions.txt:209,搜「downside」)与第 223 段(text/06-fm-functions.txt:223,搜「step = 1」)。

  24. 出处:「Functions」第 51 段(text/06-fm-functions.txt:51,搜「part of the program in which the binding is visible」)。

  25. 出处:「Functions」第 53 段(text/06-fm-functions.txt:53,搜「own little world」)。

  26. 出处:「Functions」第 55 段(text/06-fm-functions.txt:55,搜「whole function」)。

  27. 出处:「Functions」第 63 段(text/06-fm-functions.txt:63,搜「innermost」)。

  28. 出处:「Functions」第 99 段(text/06-fm-functions.txt:99,搜「lexical scoping」)。

  29. 出处:「Functions」第 184 段(text/06-fm-functions.txt:184,搜「stored on top of this stack」)。

  30. 出处:「Functions」第 186 段(text/06-fm-functions.txt:186,搜「too much recursion」)。

  31. 出处:「Functions」第 258 段(text/06-fm-functions.txt:258,搜「closure」)。 2

  32. 出处:「Functions」第 272 段(text/06-fm-functions.txt:272,搜「mental model」)与第 274 段(text/06-fm-functions.txt:274,搜「remembers this environment」)。

  33. 出处:「Functions」第 293 段(text/06-fm-functions.txt:293,搜「three times slower」)。 2

  34. 出处:「Functions」第 295 段(text/06-fm-functions.txt:295,搜「human-friendliness」)、第 299 段(text/06-fm-functions.txt:299,搜「paralyzing」)与第 301 段(text/06-fm-functions.txt:301,搜「measure」)。

  35. 出处:「Functions」第 305 段(text/06-fm-functions.txt:305,搜「adding 5 or multiplying by 3」)。

  36. 出处:「Functions」第 326 段(text/06-fm-functions.txt:326,搜「shortest」)与第 350 段(text/06-fm-functions.txt:350,搜「depth of the call stack」)。

  37. 出处:「Functions」第 385 段(text/06-fm-functions.txt:385,搜「hefty invoice」)与第 356 段(text/06-fm-functions.txt:356,搜「space for mistakes」)。

  38. 出处:「Functions」第 405 段(text/06-fm-functions.txt:405,搜「conflates three」)。

  39. 出处:「Functions」第 425 段(text/06-fm-functions.txt:425,搜「aligned tables」)。

  40. 出处:「Functions」第 429 段(text/06-fm-functions.txt:429,搜「frameworks」)。 2

  41. 出处:「Functions」第 435 段(text/06-fm-functions.txt:435,搜「easier to combine」)。

  42. 出处:「Functions」第 437 段(text/06-fm-functions.txt:437,搜「pure function」)。

  43. 出处:「Functions」第 439 段(text/06-fm-functions.txt:439,搜「pure version of console」)。