跳到主要内容

Eloquent JavaScript 4e 通读笔记(边读边写)

格式:每章一节;file:N = text/xx.txt 第 N 段;「短语」= 该段真实存在的可搜索短语。

Introduction(text/03-fm-introduction.txt, 179 段)

  • 全书主题:instructing computers;编程=构造精确指令集(3:9-15)。「program is a building of thought」(3:39)。
  • 作者立场:反对「最佳实践小圈子」——"This is not only boring—it is ineffective";好程序的品味靠练习不靠规则清单(3:43)。
  • 「Why language matters」核心演示:同一程序(1 加到 10)四种写法:二进制开关(3:49-59)→ 英文指令(3:63-81)→ 带标签+跳转的伪代码(3:85-97)→ JS while 版(3:101-109)→ sum(range(1,10)) 版(3:115-118)。结论:好语言让你在更高层谈动作:省略细节、提供构件、允许自定义构件、构件可组合(3:120)。
  • JS 史:1995 Netscape Navigator;与 Java 无关,名字是营销(3:124-126);ECMAScript 标准=同一语言两个名字(3:128);作者承认 JS「ridiculously liberal」导致错误难被发现,这是新手友善设计的反效果(3:130);但灵活性留出技术在刚性语言中不可能的空间,作者最终喜欢上 JS(3:132)。
  • 版本史:ES3(2000-2010 主流)→ ES4 激进改革 2008 被放弃 → ES5 2009 保守 → ES6/2015 大更新吸收 ES4 想法 → 此后每年小更新;不破坏旧程序是设计纪律(3:134-136);本书用 2024 版 JS(3:136)。
  • JS 不只在浏览器:MongoDB/CouchDB 查询语言;Node.js(3:138,指到 Chapter 20)。
  • 全书结构(3:152):前 12 章语言;后 7 章浏览器(ch13-19);2 章 Node(ch20-21);5 个项目章(机器人/语言/平台游戏/像素编辑器/网站)。注意:这是 3rd edition 的编号说法!4e 实际章数不同——书正文里 Chapter 20 指 Node(3:138),但 chapters.json 有 21 个正文章(intro 为 0)。待核对:4e 目录=Introduction + 21 章 + 项目穿插。3:154 说 "first four chapters" 语言基础、ch5-6 抽象,之后 error/regexp/modules/async,第二个项目章=实现编程语言(3:156);第二部分 ch13-19(3:158);ch20 Node、ch21 网站(3:160)。这些编号与 4e 实际文件偏移 1(intro 算第 0 章):4e 的 20=Node、21=skill-sharing site。与 chapters.json 一致(23-fm-node-js=Ch20,24=Ch21)。
  • 排版约定:// → 表示输出(3:174-177)。

Ch1 Values, Types, and Operators(text/04-fm-values-types-and-operators.txt, 282 段)

  • 世界观:机器世界里只有 data;一切以比特序列存储(4:9-11)。比特=两态事物(4:11);13 的二进制演示,权重 8+4+1(4:13-18)。
  • value=把比特海切成块;每个 value 有 type 决定角色(4:24)。值「召之即来」,不用就消散、比特回收(4:26)。
  • number:64 位存储;2^64≈18 quintillion(18 后面 18 个零)(4:38);1 位存符号、一些位存小数点位置 → 整数上限约 9 quadrillion(15 个零)(4:42);小数计算不精确,要当近似值对待(4:54);2.998e8 科学计数法(4:48-52)。
  • 运算符优先级:先乘除后加减,同级左结合;拿不准就加括号(4:64-72);% 取余 314%100→14、144%12→0(4:74)。
  • 特殊数:Infinity/-Infinity(无穷计算不数学可靠,会引出 NaN)(4:78);NaN=0/0、Infinity-Infinity 等;NaN 是 number 类型但不等于任何数,连自己都不等(4:80,168-173)。
  • string:三种引号;反斜杠转义 \n、\(4:90-105);基于 Unicode,每个字符一个数(4:107);JS 用 16 位/字符单元,Unicode 字符数约是其两倍 → emoji 等占两个「字符位置」(4:109,回指 Ch5);+ 是拼接不是加法 con+cat+e+nate(4:111-113);模板字面量 ${}(4:117-121)。
  • typeof;一元/二元运算符(4:125-136)。
  • boolean:比较产生;字符串比较按 Unicode 码逐字符,大写永远「小于」小写 Z<a;只有 NaN 不等于自己(4:141-173)。
  • 逻辑运算:&&、||、!;优先级 || 最低 → && → 比较 → 其余,为少写括号设计(4:177-197);三元 ?:唯一三元运算符(4:199-206)。
  • null/undefined 都表示「无意义值」;两者差异是 JS 设计事故,作者建议当可互换处理(4:210-214)。
  • type coercion 自动类型转换:8*null→0、"5"-1→4(数字)、"5"+1→"51"(拼接优先)、"five"*2→NaN、false==0→true(4:218-233);== 跨类型的复杂规则;null==undefined 为 true 但 null==0 为 false(4:235-242);作者建议防御性地用 ===/!==(4:244-246)。
  • 短路求值:|| 左边可转 true 就返回左边 → 默认值写法 null || "user";0/NaN/空串算 false(4:250-259);?? 只在 null/undefined 时取右边,常比 || 合适:0||100→100 但 0??100→0(4:261-268);&& 反向;右边只在需要时才求值(4:272-274)。
  • 小结:四种类型(number/string/boolean/undefined 值域)+运算符全家桶;够当袖珍计算器(4:276-282)。

Ch2 Program Structure(text/05-fm-program-structure.txt, 449 段)

  • expression(产生值的代码片段,可嵌套如子句)/statement(完整句子)/program=语句列表(5:15-19);纯产生值不影响世界的语句是无用的 → side effects 概念(5:26);分号:省略规则复杂易错,作者全书一律写分号(5:28)。
  • binding(=variable):捕捉并持有值 let caught = 5 * 5(5:32-38);触手比喻:binding 是触手不是盒子,不含值只是抓住值;两个 binding 可指同一个值(5:55);luigisDebt=140→还 35→105(5:57-62);空 binding 取值得 undefined(5:64)。
  • var=2015 前的旧写法,行为古怪少用;const=常量 binding(5:72-81)。
  • 命名规则:不能数字开头、可含 $ 和 _;关键字/保留字表(5:85-95)。
  • environment:某时刻所有 binding 及其值的集合;程序启动时非空(5:99)。
  • function=「包在值里的一段程序」(5:103);调用/应用;arguments(5:107);console.log=从 console 值取 log 属性(5:115)。
  • 返回值:Math.max(2,4)→4;函数调用是表达式可嵌入大表达式(5:119-127)。
  • 控制流:自上而下;Number() 把字符串转数(prompt 返回 string)(5:133-139)。
  • 条件执行:if + !Number.isNaN(theNumber);block=花括号把多条语句捆成一条;else、else if 链(5:145-190)。
  • while 循环:打印 0-12 偶数;binding 跟踪进度(5:204-219);2^10=1024 演示(5:221-230);建议从 0 开始数(Ch4 会明白)(5:232);2**10 运算符存在但「会毁了例子」(5:234)。
  • do while:至少执行一次,测条件在体后;逼用户输名字程序(5:236-244)。
  • 缩进是给人看的:视觉形状对应块形状;作者用 2 空格(5:246-259)。
  • for 循环:把循环状态三件事(初始化/测试/更新)归拢到一行(5:261-285)。
  • break 跳出循环:找 ≥20 且被 7 整除的第一个数→21(5:289-297);% 测整除(5:299);无终止条件+无 break=无限循环(5:303);continue 跳到下一轮(5:305)。
  • 快捷更新:counter += 1、counter++(5:307-325)。
  • switch:为「按值分发」而生但语法笨拙(继承自 C/Java),漏 break 会贯穿执行到下一个 case;作者直言 if 链可能更好看(5:327-352)。
  • 驼峰命名是主流;Number 大写开头=构造函数记号(Ch6 讲)(5:354-365)。
  • 注释: // 与 /* */(配魏尔伦诗句的幽默例子)(5:367-390)。
  • 练习:三角形、FizzBuzz(文中提到这是据说能筛掉大比例候选人的面试题)、棋盘(5:402-449)。

Ch3 Functions(text/06-fm-functions.txt, 488 段)

  • Knuth 题词:「计算机科学不是天才的艺术,是许多人像砌小石墙一样互相叠加」(6:5)。
  • 函数四大用途:结构化大程序、减少重复、给子程序命名、隔离;定义新词汇——成人词汇量约 2 万词,没有语言内置 2 万条命令(6:9-13)。
  • 定义:const square = function(x){...};square(12)→144(6:17-24);参数=函数内的普通 binding,初值由调用者给(6:26,47);return 立即跳出并交值;无 return→undefined(6:45);roundTo(23,10)→20(6:37-43)。
  • scope:global / local;每次调用都新建一份局部 binding → 函数间隔离,各调用互不干扰(6:51-53);let/const 块级作用域;var 函数级(pre-2015 只有函数造作用域)(6:55-61);内层遮蔽外层 halve 例子(6:63-73);lexical scoping=可见性由程序文本中的位置决定(6:99);嵌套作用域 hummus/ingredient(6:79-97)。
  • 函数是值:可存进别的 binding、可当参数传;launchMissiles 在 safeMode 下被换成空函数(6:103-112)。
  • 声明式写法 function square(x){}:概念上被移到作用域顶部,先用后定义也行(6:118-134,future() 例子)。
  • 箭头函数 =>:2015 加入,主要为了少打字;「语言里同时有箭头函数和函数表达式没有深层原因」(6:136-158);单参数可省括号、体为单表达式时隐式 return(6:147-150)。
  • call stack:每次调用把「从哪跳来的」上下文压栈,返回时弹栈;栈耗内存,太大→"out of stack space"/"too much recursion";chicken()↔egg() 互调爆栈演示(6:160-195)。
  • 参数极宽容:多传的被忽略 square(4, true, "hedgehog")→16;少传的补 undefined;坏处是传错没人告诉你(6:199-209);minus(a,b) 一两参通吃(6:211-219);默认参数 roundTo(n, step=1)(6:221-231)。
  • closure:wrapValue(n) 返回 () => local,wrap1()→1、wrap2()→2,各调用局部 binding 各自存活(6:244-256);定义=「引用外围作用域中特定一份局部 binding 的能力」(6:258);multiplier(2) 得 twice,twice(5)→10(6:262-268);心智模型:函数值=代码+它出生时的环境;调用时看到的是出生环境,不是调用环境(6:272-274)。
  • 递归:power(2,3)→8;典型实现里比 for 循环慢约 3 倍(6:280-293);速度 vs 优雅=对机器友好 vs 对人友好的连续谱(6:295);建议:先写对、写懂,担心慢就先测量再说——多数代码根本跑不到值得优化的次数(6:301);分支探索问题真适合递归(6:303);findSolution:从 1 出发 +5 或 ×3 凑目标数;find(1,"1") 走查到 13→(((1 * 3) + 5) + 5)(6:305-350);不保证最短,找到一个就收工(6:326)。
  • 函数怎么长出来:①重复代码→提炼;②先起名再写体(6:354-358);农场清单案例:007 Cows/011 Chickens→加了猪→先提炼成 printZeroPaddedWithLabel(名字把三件事搅在一起)→再提炼成单一概念的 zeroPad(number,width),更通用可复用(6:362-425);原则:不确定需要就不加聪明;抵制给每个功能写「框架」的冲动(6:427-429)。
  • 纯函数:无副作用也不依赖别人的副作用;同参必同果;调用可被其返回值替换;测一处=处处可用;非纯函数测试要更多脚手架(6:433-437);但不必为不纯羞愧——console.log 没有纯版本,可它很好用(6:439)。
  • 练习:min、递归 isEven(50/75/-1)、countBs/countChar(6:462-488)。

Ch4 Data Structures: Objects and Arrays(text/07-fm-data-structures-objects-and-arrays.txt, 706 段)

  • Babbage 题词(进错数出对数?)(7:5)。
  • 狼人松鼠故事(Jacques 晚 8-10 点变松鼠,记日志找触发因素)——全章的主线例子(7:17-27)。
  • array:[2,3,5,7,11];下标从 0 起,索引=从头要跳过的元素个数(7:35-47)。
  • property:几乎所有值都有属性,null/undefined 例外(取属性→TypeError)(7:51-56);点号=字面名,方括号=先求值再当属性名(7:58-62);数组元素就是以数字为名的属性(7:62)。
  • method=存在属性里的函数;toUpperCase(Doh→DOH)(7:68-78,Ch6 揭秘 this);push/pop;栈=后进先出,呼应上一章 call stack(7:84-96)。
  • object:花括号+属性列表;day1.squirrel→false、day1.wolf→undefined 再赋值(7:102-114);花括号有两义:句首=语句块,其他位置=对象;n => {prop: n} 陷阱,要加括号(7:116-123);对象=有名字的触手的章鱼(7:129);delete 与 in(置 undefined ≠ 删除,in 仍 true)(7:131-144);Object.keys/Object.assign(7:146-156);数组就是对象,typeof [] → "object";长而扁、触手按数字排队的章鱼(7:158)。
  • journal=对象数组(7:160-173)。
  • 可变性:number/string/boolean 不可变("cat" 变不成 "rat");对象可变(7:179-181);object1==object2 同一身份 true、object1==object3 内容相同仍 false;== 比的是身份不是内容;JS 没有内建深比较(7:183-210);const score 可改属性不可改指向(7:202-208)。
  • 相关性主线:phi 系数(ϕ),2×2 频率表 [76,9,4,1] (pizza 表);ϕ≈0.069 极小 → 披萨无关(7:235-253);phi(table) 函数直接翻译公式(7:261-272);tableFor:每个条目算 index=(squirrel?2)+(event?1),落到四个格子(7:278-292);for-of 循环(7:309-315)。
  • 终局分析:全事件相关性→过筛 |corr|>0.1→peanuts 0.59、brushed teeth -0.38→造组合事件「peanut teeth」→ϕ=1→真相:吃花生且没刷牙(7:319-379);Jacques 戒花生变回去,但最终选择当全职松鼠(树屋+十年花生酱)(7:381-383,全书最有名的幽默结局)。
  • 数组方法:unshift/shift(任务队列)(7:389-402);indexOf/lastIndexOf(-1=没找到)(7:404-411);slice(含头不含尾)(7:413-420);concat;remove=slice+concat 不改原数组(7:422-433)。
  • 字符串:kim.age=88 不生效——原始类型存不了属性(7:437-444);slice/indexOf(可搜多字符)/trim/padStart(=上一章 zeroPad)/split/join/repeat(7:446-481)。
  • rest 参数 max(...numbers)→9(7:492-504);展开调用 max(...numbers)→7(7:506-512);数组/对象字面量展开 {...coordinates, y:5, z:1} 后者覆盖(7:514-524)。
  • Math 对象=命名空间,防全局名字污染;let/const 重名会拦,但 var/function 与内建重名不拦(perversely)(7:528-534);Math.random=伪随机:机器存一个隐藏值,每次对它做复杂运算派生新数(7:548-564);floor/ceil/round/abs(7:566)。
  • 解构:phi([n00,n01,n10,n11]) 直接在参数表里拆数组;let {name} = obj(7:570-596);解构 null/undefined 报错(7:596)。
  • 可选链 a?.b(7:598-617)。
  • JSON:对象数组在内存里是地址纠缠,要存/传得序列化成扁平描述;JSON 限制:属性名必须双引号、不许函数/计算、不许注释(7:619-627);JSON.stringify/parse(7:636-643)。
  • 练习:Vec vector、Group(Set 已被占名)、iterable groups(9:563-589)。

Ch6 The Secret Life of Objects(text/09-fm-the-secret-life-of-objects.txt, 589 段)

  • Liskov 题词:抽象数据类型=以操作定义类型(9:5)。
  • OOP=以对象为程序组织核心的一组技术;没人真正认同它的精确定义(9:9)。
  • 抽象数据类型=家电比喻:搅拌机设计师懂的材料学/电学被塑料壳盖住,用户只需懂几个旋钮(9:15-17);interface=外部代码能做的操作集合,其余细节 encapsulated(9:21);连数字都能当 ADT;作者抱怨经典 OOP 把「单个对象」当组织单位有点不幸——有用的功能常常是一组类紧密协作(9:23)。
  • methods=持有函数值的属性;this=以特殊方式传入的额外参数;函数被当方法调用时 this 指向被调用的对象;speak.call(whiteRabbit, "Hurry") 显式给 this(9:29-45);箭头函数不绑自己的 this、看外围作用域的 this;用 function 写 some 回调就找不到 this.value(9:49-62)。
  • prototypes:对象可以链到别的对象,没有的属性就去原型上找,一路找到没有原型的对象为止;Object.prototype 的原型是 null(9:70-87);函数派生自 Function.prototype、数组派生自 Array.prototype(9:89-97);Object.create(proto)(9:99-111)。
  • classes:class 记法=构造函数+原型的打包;2015 才引入,之前靠普通函数+手工操作 prototype 属性;所有非箭头函数天生带一个空的 prototype 属性(9:127-156);构造函数名大写是约定(9:156);关键区分:构造函数的 prototype 属性(给实例用)vs 构造函数自己的原型(Function.prototype)(9:158-165);字段声明 speed=0 直接加到实例(9:167-174)。
  • 私有属性 #:只能在类声明内部调用/使用;私有实例属性必须声明,普通属性可以随手赋值造出来(9:184-214)。
  • 覆盖原型属性:teeth 例子——Rabbit.prototype.teeth="small",killerRabbit 自己的 teeth 挡住原型;原型是背景板(9:218-233);[1,2].toString()→"1,2" 而 Object.prototype.toString.call([1,2])→"[object Array]"(9:235-246)。
  • 用普通对象当 map 是危险的:"toString" in ages → true(原型链漏进来);Object.create(null) 无原型安全(9:254-272);Map 类任意类型键 set/get/has(9:276-290);Object.keys 只列自有属性;Object.hasOwn 忽略原型(9:292-297)。
  • polymorphism:面向接口写的代码,任何支持该接口的对象都能插入;String(对象) 调 toString;array-like 接口(length+数字属性)连字符串都支持(9:301-322)。
  • getter/setter/static:varyingSize.size 每次读都调方法(9:330-339);Temperature 华氏/摄氏互转存一份;Temperature.fromFahrenheit(212)→100°C(static 存在构造函数上)(9:343-374)。
  • symbols:2015 加入,每次 Symbol() 造的值独一无二,适合做不打架的接口属性名(9:378-395);[length] 计算属性名(9:405-414)。
  • iterator 接口:for/of 期望对象有 Symbol.iterator 方法→返回带 next() 的迭代器→next 返回 {value, done}(9:418-423);"OK" 手动迭代演示(9:426-432);List+ListIterator 实现(9:436-480);[..."PCI"]→["P","C","I"] (9:494-495)。
  • inheritance:extends/super;LengthList 缓存 length;作者立场明确:封装与多态被普遍认为是好主意,继承更有争议——它把类绑在一起制造纠缠;不该是第一工具,不该主动找机会造类族谱(9:497-529);instanceof 穿透继承层级(9:533-545)。
  • 练习:Vec 向量类、Group(Set 已被占名)、iterable groups(9:563-589)。

Ch7 Project: A Robot(text/10-fm-project-a-robot.txt, 290 段)

  • Dijkstra 题词:「机器能否思考」与「潜艇能否游泳」一样不相干(10:5)。
  • Meadowfield 村:11 地点 14 条路;graph=点+线;roads 字符串数组→buildGraph→每个地点到可达地点的映射,Object.create(null) 造无原型 map(10:15-49)。
  • 任务:送包裹机器人;模拟需要虚拟世界模型:机器人在哪+未送包裹在哪(10:53-57)。
  • 重要设计判断:「听起来像对象的东西不自动该是程序里的对象」——给每个概念反射式写类,得到一堆各自带内部可变状态、互相连接的对象,难懂易碎;"This is wrong. At least, it usually is."(10:59-61);把村庄状态压缩成最小集合:机器人位置+未送包裹列表(10:63);不修改状态,每次移动计算新状态(10:65-84);VillageState.move=map(带走)+filter(送达);旧状态原封不动(first.place 仍是 Post Office)(10:92-105)。
  • persistent/immutable 数据(10:107-122);Object.freeze 让写入被忽略但「写入被忽略和写入出错一样让人糊涂」,作者更愿意口头约定别乱动(10:111-117);动机:对象不变才能孤立推理(同一起点移到 Alice 家永远得到同一新状态);「我们能造多复杂的系统,最重要上限是我们能理解多少」(10:118-120);承认语言不帮忙时设计持久结构更难,本书两种都用(10:122)。
  • 机器人=函数:(state, memory) → {direction, memory}(10:126-128);runRobot 循环到包裹送完(10:130-141)。
  • 三代机器人:randomRobot(最笨但可行的策略,随机走,63 turns 送达 5 包裹)(10:145-185);routeRobot(固定邮路跑两遍,上限 26 turns,用 memory 存剩余路线)(10:191-209);goalOrientedRobot(findRoute 找最短路,约 16 turns)(10:248-262)
  • 寻路=搜索问题:能验证一条路线对不对,却不能像 2+2 那样直接算出答案,只能不断生成候选直到撞上可行的(10:217);重复访问同一地点的路线绝不高效,剪掉(10:219);广度优先:从起点让「已知路线的网」均匀向外爬、绝不缠回自己,第一根碰到终点的线就是最短路;work list 保证先到先探(10:221-246);不处理工作列表耗尽,因为图是连通的(10:246)。
  • 练习:compareRobots(100 个任务两边都跑,公平比较)、改进机器人、PGroup 持久集合(10:264-290)。

Ch8 Bugs and Errors(text/11-fm-bugs-and-errors.txt, 406 段)

  • Kernighan 题词:调试比写代码难一倍;所以用尽聪明写出的代码,按定义你已聪明到调不了它(11:5)。
  • bug 二分:想法本身糊涂 vs 想法转成代码时出错;前者更难诊断(11:11)。
  • JS 宽松是障碍:true * "monkey" 不报错;荒谬计算得 NaN/undefined 程序照样跑,错误在坏值旅行了几个函数之后才显形,甚至只是悄悄把输出弄错(11:15-19)。
  • strict mode:"use strict";忘 let → ReferenceError;类和模块里自动严格(11:27-39);非方法调用 this=undefined;Person 忘 new 的对照(11:41-57);class 构造器没 new 必报错(11:59);禁同名参数、删 with(11:61)。
  • Types:有的语言运行前查类型;JS 运行时才看还乱转换;类型注释 (graph: Object, from: string, to: string) => string[];泛型 T 的必要性(randomPick (T[])→T);推荐 TypeScript;「本书继续用原始、危险、无类型的 JS」(11:67-84)。
  • Testing:自动测试=写程序测程序;写完获得超能力:几秒验证所有测过的场景(11:92);test(label, body) 迷你框架;希腊语/阿拉伯语 toUpperCase 测试(11:96-108);test runner(11:110);交互的外部对象越多越难测;上一章持久值风格好测(11:112)。
  • Debugging:抵抗乱改代码的冲动;先想:分析→提出理论→再做观察验证理论(11:141);numberToString(13,10) 输出 1.5e-323… 的 bug:13/10 不是整数;n /= base 应为 Math.floor(n / base);策略性 console.log 打出 13→1.3→0.13(11:124-152);断点与 debugger 语句(11:154-156)。
  • Error propagation:promptNumber 输入 orange 返回 null;特殊值的两个缺点:①函数本来就可能返回任何值时要用对象包(lastElement {failed:true},迭代器 next() 同款)(11:178-186);②调用 10 次要查 10 次 null(11:188)。
  • Exceptions:throw/catch;unwinding the stack=异常沿着调用栈一路炸下去,丢掉沿途所有调用上下文(11:194);catch=栈上的障碍物(11:196);promptDirection/look(11:200-219);Error 自带 message+stack trace(11:223);最大优点:只有出错点和处理点需要写错误处理,中间函数全部免责(11:225)
  • 清理:银行 transfer 先扣后问,问名抛异常→钱凭空消失(11:237-261);少副作用帮大忙(11:263);finally+progress 变量回滚(11:267-284);写异常可靠的程序很难,很多人干脆不做;好不好取决于软件挂了伤害多大(11:286)。
  • Selective catching:未捕获异常→浏览器控制台/Node 直接中止进程(11:290);程序员错误:让它崩(11:292);日常可预期的失败:崩是糟糕策略(11:294);JS 明显缺失:不能选择性捕获,要么全抓要么不抓(11:300);typo promtDirection→catch 全收→无限循环+埋掉真错误(11:304-314);规则:除非为了「路由」异常,不要一网打尽(11:316);比对 message 字符串做程序决策很脆(11:320);InputError extends Error + instanceof 不匹配就 rethrow(11:324-351)。
  • Assertions:为程序员错误服务不是为正常运转服务;firstElement 空数组就大声炸;别给每种坏输入都写断言,代码会吵死(11:353-368)。
  • 练习:Retry(20% 抛异常的 primitiveMultiply)、locked box 的 withBoxUnlocked(finally 语义)(11:378-406)。

Ch9 Regular Expressions(text/12-fm-regular-expressions.txt, 643 段)

  • Zawinski 题词:「我知道了,用正则!——现在他们有两个问题了」(12:5)。
  • 工具以混乱的进化方式存活传播:胜出的不一定是最好的,而是恰好够用且搭上了成功技术便车的(12:9);正则=字符串模式的小型独立语言;又笨拙又有用;语法晦涩、JS 的接口粗糙(12:11-13)。
  • 两种写法 /abc/ 与 new RegExp("abc");斜杠记法保留反斜杠、/ 要转义(12:17-28);test 布尔(12:32-39)。
  • 字符集 [0-9] (按 Unicode 序)、[^\d] 取反;捷径表 \d \w \s \D \W \S .(12:45-70);dateTime 正则「看起来完全可怕——一半是反斜杠噪音」(12:74-80)。
  • 国际化:\w 只认 26 个拉丁字母+数字+下划线(é、β 都不算);\s 因「奇怪的历史意外」没问题;\p{L}/\p{Script=Greek} 要加 u 旗标才生效;处理非英文文本用 \w 是负债;但要转数字的场合要 \d(Number 不认任意数字字符)(12:92-121)。
  • 重复:+ ≥1、* ≥0(永不阻止匹配)、? 可选 neighbou?r、{2,4} 精确次数(12:123-156);分组 (hoo+)+、/i 忽略大小写(12:158-168)。
  • exec 返回数组+index;分组文本依次进数组;未匹配组=undefined;重复组只留最后一次;(?:) 不捕获(12:170-203)。
  • Date 类:月份从 0 起日期从 1 起——「令人困惑且愚蠢,当心」(12:223);时间戳=1970 起毫秒(Unix time),getTime(12:227-234);getYear(减 1900)基本没用(12:236);getDate 用 exec+解构(12:240-248)。
  • 边界:^ $ \b(\b 依赖 \w 所以不可靠);look-ahead (?=) 不前进匹配位置(12:250-267);选择 |(12:269-281)。
  • 匹配机制:从每个起点试;把正则当流程图:能从左走到右即匹配(12:283-289)。
  • 回溯:进岔路时记住位置,失败就退回试另一条;"103" 走二进制→十六进制→十进制分支(12:291-299);.*x 对 abcxe 逐步退(12:301);灾难性回溯:([01]+)+b——每多一个字符工作量翻倍,几十个字符=实际永远跑不完(12:303-305)。
  • replace:字符串/正则/g 全局;$1 $2 $& 引用分组;替换参数可以是函数(minusOne:1 lemon, 2 cabbages, 101 eggs → no lemon, 1 cabbage, and 100 eggs)(12:307-351)。
  • 贪婪:stripComments 第一版 [^]* 一路吃到最后一个 / 把代码吃了;+?{} 默认贪婪(尽量多吃再退),加 ? 变非贪婪(尽量少吃不够再添);大量正则 bug 源于用错贪婪,建议默认非贪婪(12:353-383)。
  • 动态构造 RegExp:特殊字符要转义 escape 正则(12:385-406);search(12:408-417)。
  • lastIndex:仅 g/y+exec 时生效;「混乱是 JS 正则接口的本质特征」(作者原话式的嘲讽);共享正则对象残留 lastIndex 的坑:digit.exec 第二次→null(12:419-452);g 改变 match 行为返回所有匹配(12:454-457);对全局正则保持警惕:只在 replace 和明确要 lastIndex 时用(12:459);matchAll(12:461-472)。
  • INI 解析实战:parseINI;^ $ 锚定整行——漏掉会「大体能用但某些输入行为怪异,难查」(12:534);if (match = line.match(...)) 赋值即判定的惯用法(12:536)
  • code units:/🍎{3}/ 无 u 不匹配(苹果=两个单元,{3} 只作用于第二个);<.> 匹配不了 <🌹>,加 u 就行(12:540-556)。
  • 总结表+「利器配笨手柄;别硬塞它表达不了的东西」(12:558-606)。
  • 练习:regexp golf、引号风格、JS 数字正则(12:608-643)。

Ch10 Modules(text/13-fm-modules.txt, 287 段)

  • Tef 题词:「写容易删的代码,不是容易扩展的代码」(13:5)。
  • 程序有机生长→纠缠的两个实际恶果:①无法孤立理解任何一块;②想复用某块,重写比重写……原文:rewriting it may be easier than disentangling(13:11-13);big ball of mud:一挑就散,只做成一团糟(13:15)。
  • module=声明依赖+接口的程序片段;显式交互 → LEGO(定义好的接口拼接)vs 泥(什么都黏什么)(13:19-25)。
  • ES modules:原初 JS 无模块概念,所有脚本同一全局作用域→重名互踩(13:29);ES2015 起 scripts vs modules;import/export 只能在模块顶层、加载时立即解析(13:31-58);as 改名、default export、import * as(13:60-78);名字解析因平台而异:浏览器=URL,Node=文件(13:56)。
  • Packages:复制代码→修 bug 只修一份;NPM=在线仓库+随 Node 附带的命令行工具;写作时 300 万+ 包,「相当大一部分是垃圾」(13:84-96);许可证:默认你拥有版权;JS 社区主流是宽松许可证(只要求随代码保留声明)(13:102-104);ini 包(13:108-111)。
  • CommonJS:2015 前社区自造;IIFE 包住整个模块造局部作用域(13:115-134);require+exports(13:138);formatDate 例子用 ordinal+date-names 包(13:140-166);require 简化实现:cache+Function 包装器(13:172-189);Function 构造器=从字符串造程序,强大且危险:能被骗进去的字符串=任意代码执行(13:183);关键差异:ES import 在模块代码运行前解析;require 是普通函数,运行中调用,依赖名可以是任意表达式(13:191);过渡缓慢粗糙;「没有真正的理由再用 CommonJS 写新程序」(13:193)。
  • 构建与打包:TypeScript 等方言编译成(甚至旧版)JS(13:197);200 个文件每个 50ms=10 秒 → bundler 合成大文件;minifier 删注释空白、改变量名(13:199-201);NPM 上/网页上的代码常经过多轮变换(13:203)。
  • 模块设计:好用=简单可预测;ini 包模仿 JSON 的 parse/stringify 约定(13:213);专注做一件事、算出值而不是搞副作用 → 更可组合;内建读文件的 INI 解析器在浏览器里废掉(13:215-217);「先造对象→把文件装进对象→用专门方法取结果」的有状态仪式:it's terrible(13:219);数组够用就用数组(13:221);图数据结构不匹配:dijkstrajs 要带权边,适配=每条边权重 1(13:227-243);可组合性障碍:不同包用不同数据结构描述同类东西;要设计得可组合,就去看别人用什么数据结构、跟着用(13:244);探索期别急着组织,稳固后再整理(13:246)。
  • 练习:模块化机器人、roads 模块、循环依赖(CommonJS 容忍有限循环:加载完成前互不访问接口即可)(13:256-287)。

Ch13 JavaScript and the Browser(text/16-fm-javascript-and-the-browser.txt, 165 段)

  • Berners-Lee 题词:万维网之梦=共享信息空间,链接可以指向一切(16:5)。
  • 没有浏览器就没有 JavaScript——或者有也没人理它(16:9);Web 技术去中心化演化:各厂商先乱加功能→有时被别人采纳→最后写进标准;福祸相依,系统内部一致性差(16:11-13)。
  • 网络:互联网=1980 年代;protocol=对通信方式的约定(16:17-23);HTTP 请求行长这样:GET /index.html HTTP/1.1(16:25-29);协议叠协议:HTTP 假设网络是「按序送达的比特流」,兑现这个假设的是 TCP——端口、server/client、双向管道,TCP 是对网络的抽象(16:31-39)。
  • Web≠互联网;URL=protocol+server+path(16:43-53);IP 地址与域名;作者自己注册了 eloquentjavascript.net(16:55)。
  • HTML:标签/属性/实体(< > &);HTML 解析极其容错且容错方式已标准化:缺的标签浏览器自动补(16:61-109);doctype 与 charset 忘写浏览器会做荒谬事(16:113)。
  • script 标签:读到就执行;src 引外部文件;忘写 则页面其余部分被当脚本文本(16:117-131);type="module" 加载 ES 模块(16:133);onclick 属性(16:135-139)。
  • 沙箱:浏览器严格限制 JS 能做的事——不能碰你电脑上的文件、只能动它嵌身的页面;「想象一个装了粗钢条笼子的沙箱」(16:143-151);难点=给程序足够空间有用、又不让它做危险事;猫鼠游戏史(16:149-151)。
  • 浏览器战争:Mosaic→Netscape→IE;约 2003 年「兼容性黑暗时代」:两三个互不兼容的平台,bug 还各不相同(16:155-157);Firefox/Chrome/Safari 四强并立,新玩家更重视标准(16:159);2018 年 Edge 变成 Chrome 内核的壳;市场份额集中可能再次让单一厂商(Google)把它的 Web 观推给全世界(16:163)。

Ch14 The Document Object Model(text/17-fm-the-document-object-model.txt, 471 段)

  • 尼采题词(17:5)。
  • 浏览器解析 HTML(像 Ch12 的解析器)并建文档结构模型;DOM=活的数据结构:一改,屏幕上的页面跟着变(17:9-11);document.documentElement/head/body(17:34)。
  • tree 定义:分支结构+无环+唯一根;树也用来维护有序数据(查找/插入比扁平数组高效)(17:38-46);nodeType 码:元素 1、文本 3、注释 8(17:48)。
  • 对标准的批评:DOM 不是为 JavaScript 设计的——它是语言中立接口,还要管 XML;数字码不像 JS;作者判断:「与所用语言深度整合的接口,比跨语言一致的接口省更多时间」(17:54-58);childNodes 是 NodeList 不是真数组,没有 slice/map(17:60);没有「一步造出带子节点和属性的节点」的 API → 全靠副作用逐个加,代码冗长重复难看;但缺陷不致命:JS 能自造抽象,库就是这么来的(17:62-64)
  • 走树:parentNode/childNodes/firstChild/lastChild/previousSibling/nextSibling;children 只含元素节点(17:70-74);talksAbout 递归扫描文本节点(17:78-92)。
  • 空白也是文本节点:示例 body 的孩子不是 3 个而是 7 个(空格算数);按固定路径取节点=把文档结构假设焊死进程序(17:98);getElementsByTagName/getElementById/getElementsByClassName(17:102-117)。
  • 改文档:remove/appendChild/insertBefore/replaceChild;一个节点在文档里只能存在一处——插入即移动(Three/One/Two)(17:132)
  • 造节点:replaceImages(alt 文本替换图片)从后往前循环——getElementsByTagName 返回的是 live 列表,文档一变它就变,从前往后会跳元素;要固定快照用 Array.from(17:160-167);createElement;elt 工具函数(17:171-195)。
  • 属性:标准属性可直接点;自定义属性用 getAttribute/setAttribute,约定加 data- 前缀;class 因 JS 关键字历史原因叫 className(17:201-219)。
  • 布局:block vs inline;offsetWidth/Height(含边框)vs clientWidth/Height(不含):3px 边框的段落 19 vs 25(17:223-241);getBoundingClientRect+pageXOffset(17:245);浏览器尽量拖着重算布局;改一改读一读交替进行会逼出大量布局计算(layout thrashing): naive 32ms vs clever 1ms 拼 2000px 的 X(17:247-276)
  • 样式:style 属性;display: none 是隐藏元素的首选方式(比删除好恢复);style 对象连字符属性转驼峰 fontFamily(17:282-311)。
  • 层叠(CSS):多条规则合成最终样式;同特异性时后读的赢;style 属性最高;特异性=描述精确程度(p.a > p 或 .a);p > a 直属 vs p a 任意后代(17:313-346)。
  • querySelectorAll(返回的 NodeList 不 live)/querySelector(第一个或 null)(17:348-377)。
  • 定位与动画:position static/relative/absolute(absolute 脱离文档流,相对最近的非 static 祖先)(17:381);猫沿椭圆飞舞:requestAnimationFrame ~60 次/秒;直接 while 改 DOM 页面会冻结——浏览器在 JS 运行时不刷新不响应;按时间差算增量,防止别的任务卡一下造成顿挫;弧度制;样式数值要带单位(px),不带单位的数字被忽略——0 除外(17:383-420)。
  • 练习:建表格、自写 getElementsByTagName、猫的帽子(17:430-471)。

Ch15 Handling Events(text/18-fm-handling-events.txt, 499 段)

  • Marcus Aurelius 题词(18:5)。
  • 轮询 vs 通知:轮询=程序不停查队列,漏检=不响应,大多数程序员躲着走;更好的机制=系统主动通知:注册 handler(18:13-19)
  • addEventListener(window/元素);onclick 属性每节点只能一个,addEventListener 可挂任意多个;removeEventListener 必须传同一个函数值 → handler 要起名字(18:32-61)。
  • event 对象:button 0/1/2;type 字符串(18:65-81)。
  • 传播:子节点的事件也会被祖先的 handler 看到;更具体的 handler 先跑;从事发节点向外传到根,最后 window;stopPropagation 拦住;target=事发节点 → 「宽网」模式:外层挂一个 handler 看 target,不必给每个按钮都挂(18:85-119)
  • 默认行为:preventDefault;作者警告别没正经理由破坏用户预期;有些事件根本拦不住(Chrome 的 ctrl-W)(18:123-140)。
  • 键盘:keydown/keyup;按住会连续触发 keydown;焦点决定事件落在哪个节点,没焦点时是 document.body;修饰键 shiftKey/ctrlKey/altKey/metaKey(18:144-175);用键盘事件猜输入不可靠:安卓虚拟键盘不发 key 事件、输入法(IME)多击合成一字;要内容请听 input 事件(18:177-179)
  • 指针:mousedown/up/click;click 落在同时包含按下与松开的最具体节点上(在 A 段落按下、B 段落松开,click 落在包含两者的祖先)(18:189);dblclick;clientX/Y(窗口)vs pageX/Y(文档);画点程序(18:187-217)。
  • 拖拽条:mousemove 挂在 window(拖出去也要跟);buttons(复数)=当前按着的键码之和 左1 右2 中4;与 button 单数的顺序不一致——「一致性不是浏览器编程接口的强项」(18:231-259)
  • 触摸:早期浏览器假装触摸=鼠标;假象不稳:没有多键、不碰屏就追踪不到、多指并发;touchstart/move/end;touches 是列表每指一组坐标(18:263-299);触摸 handler 里常要 preventDefault(防滚动+防鼠标事件重复触发)。
  • scroll:进度条例子;position: fixed 不随滚动;innerHeight;scroll 上 preventDefault 拦不住滚动——handler 在滚动发生之后才被调(18:303-332)
  • focus/blur:不传播(父元素收不到)(18:336-338);load 事件用于初始化、也不传播;beforeunload 的 returnValue 对话框——多数浏览器已不再显示自定义文案,因为恶意站点拿它骗人留着看减肥广告(18:366-370)
  • 事件与事件循环:handler 要等别的脚本跑完;事件循环被占满=页面又慢又卡;web worker=并行的另一个 JS 进程、不共享任何数据、只能互发消息(且只能发 JSON 化的值,对面收到的是副本)(18:374-397)
  • 定时器:setTimeout/clearTimeout、setInterval/clearInterval、cancelAnimationFrame(18:401-423)。
  • 防抖(debouncing):高频事件(mousemove/scroll/input)别每响必应;两种模式:①等停顿再干活(clearTimeout+setTimeout 500ms);②限制最快响应间隔(250ms 才更新一次坐标)(18:427-459)。
  • 练习:气球(💥)、鼠标尾迹、标签页(18:475-499)。

Ch16 Project: A Platform Game(text/19-fm-project-a-platform-game.txt, 657 段)

  • Iain Banks 题词;作者自嘲:不祝任何人以游戏编程为业——想入行的人数与实际需求严重失衡,环境不健康;但写着玩很有趣(19:9-11)。
  • 游戏基于 Thomas Palef 的 Dark Blue:极简(方块世界);跳起后还能在空中变向,不真实,但给玩家「直接操纵化身」的手感(19:21);背景=静态网格(empty/wall/lava),活动元素坐标可以是小数 → 平滑移动(19:23);用 DOM 画游戏:比你想的更能扛(19:33)。
  • 关卡=ASCII 字符画:. 空 # 墙 + 岩浆 @ 玩家 o 金币 = 横向岩浆 | 纵向 v 下滴(19:43-58);收集全部金币过关,碰岩浆重来(19:58)。
  • Level 解析:rows=字符二维数组;background=类型字符串;levelChars 把字符映射到类型名或「带 static create 的类」——同一条数据通路两种产物(19:91-93)
  • State 持久:update 造新 State(19:97-115);actor 接口:size/pos/update/type + static create(19:119-125)。
  • Vec:plus/times;size 放原型上而不是 getter——getter 每次读都新建 Vec 浪费;字符串不可变所以没事(19:165);玩家出生点上移半格(脚贴地面)(19:163)。
  • Lava:= (2,0)、| (0,2)、v (0,3)+reset;碰壁:有 reset 跳回原点,没有则速度 × -1 反弹(19:171-191);Coin:wobble=basePos+sin 偏移;随机初相位 × 2π,免得所有金币同步起伏(19:195-215)。
  • 绘制与显示解耦:DOMDisplay 只是显示器之一,下一章换 canvas 实现(19:237);scale=20 格→像素;syncState 每帧全部重画 actor:不做元素复用的记账,数量少重画便宜(19:316);状态当类名挂容器(.lost .player 变焦红、.won 白色光环 box-shadow)(19:326-335)。
  • scrollPlayerIntoView:margin=宽度 1/3 的「中性区」,不把玩家死钉在屏幕中央——跳动时画面会一直上下颠;scrollLeft 给 -10 会自动归 0,越界坐标交给 DOM 钳制(19:348-377)
  • 运动与碰撞:把时间切成小步,每步位移=速度×步长(秒制);碰撞的笨办法=移动前先测会不会进墙,进墙就取消整个动作——时间步必须小,否则玩家悬空;更好的做法(找精确碰撞点)更难,这里选择用小步长把问题藏起来(19:393-401)
  • touches:floor/ceil 求矩形覆盖的格子范围;界外一律算墙——防止走出世界+防止数组越界(19:405-424);overlap=两轴投影都重叠(19:454-459);Coin.collide 过滤后无金币→won(19:467-472)。
  • Player.update:X/Y 轴分开处理(撞地不该挡横移,撞墙不该停坠落);playerXSpeed=7、gravity=30、jumpSpeed=17;只有「在下落中按上」才起跳;「这些常数是试出来的,哪个手感好用哪个」(19:510-538)
  • trackKeys:keydown/keyup 共用一个 handler,按 event.type 写 true/false;preventDefault 防滚动(19:548-564)。
  • runAnimation:单帧步长上限 100ms——标签页隐藏时 rAF 暂停,回来若把整段隐藏时间当一步推进,玩家会穿地(19:572-587);runLevel 返回 promise,结束后留 1 秒看结果;runGame=async 循环,won 才 level++(19:591-626)。
  • 练习:命数、esc 暂停(顺带修 trackKeys 的 handler 泄漏)、monster 踩头(19:636-657)。

Ch17 Drawing on Canvas(text/20-fm-drawing-on-canvas.txt, 623 段)

  • Escher 题词:「绘画是欺骗」(20:5)。
  • 三条画图路线:DOM 样式(上一章)、SVG、canvas;关键差异:SVG 保留形状的原始描述(随时移动缩放),canvas 落笔即变成像素、忘掉它画过什么——想挪只能擦掉重画(20:9-15)
  • SVG:xmlns 换命名空间;circle/rect 造的也是 DOM 元素,能 setAttribute 改色(20:21-37)。
  • canvas:width/height 属性;空白=全透明;getContext("2d") 拿绘图接口;三种风格 2d/webgl/webgpu(书只讲 2d)(20:41-47);坐标系 (0,0) 在左上、y 向下(20:63)。
  • fill vs stroke;颜色与线宽不是方法参数,而是 context 的属性(fillStyle/strokeStyle/lineWidth)——这是 canvas 接口最反直觉的一点(20:67-75);默认画布 300×150(20:88)。
  • path 完全靠副作用描述:路径不是可以存起来的值,是一串方法调用的累积;beginPath→moveTo/lineTo→stroke/fill;fill 时未闭合路径自动补一条回到起点的线(三角形只画了两条边)(20:90-123)
  • 曲线:quadraticCurveTo 一个控制点(线被它「吸引」但不过它);bezierCurveTo 两个控制点;控制点常要试错(20:127-164);arc(圆心、半径、起止角,弧度制,从右侧起顺时针;0 到 7 画整圆)(20:166-183)。
  • 饼图:EconomiCorp 满意度调查;角度=占比×2π;从 -0.5π 开始(顶部)(20:187-219);fillText/font/基线/textAlign/textBaseline(20:227-239)。
  • 图像:矢量图 vs 位图;drawImage;九参数 drawImage=源矩形→目标矩形;把一套角色动作姿势打包进一张图,按帧取不同片段=走路动画;cycle=(cycle+1)%8(20:263-293);clearRect 擦除。
  • 变换:scale/rotate/translate 改坐标系;变换会叠,顺序有影响(先转再移≠先移再转);负缩放绕 (0,0) 翻转;flipHorizontally=平移到镜面→scale(-1,1)→平移回来——「在镜像宇宙里把 y 轴搬回原位」(20:299-348)。
  • save/restore=变换状态栈;branch 递归画分形树:没有 save/restore,第二条树枝会接在第一条最深处的末端上——形状也许有趣,但肯定不是树(20:352-382)
  • 回到游戏:CanvasDisplay 与 DOMDisplay 同接口;自己管 viewport+flipPlayer(停下也面朝最后移动方向)(20:388-408);canvas 版必须每帧重画背景——像素没有「移动」可言;Math.max/min 把视口钳制在关卡内(20:428-447);不等贴图加载:对未加载的图 drawImage 直接什么都不画,前几帧缺图无所谓(20:493);走=Date.now()/60 取模切换帧、跳=竖直速度非零、站=第 9 帧(20:495-526)。
  • 选图形接口:HTML 简单、和文字配合最好;SVG 任意缩放清晰、每个元素可挂鼠标事件;canvas 每形状成本低,海量小元素/逐像素效果(光线追踪、后处理)只有它行;可以混用(图用 SVG/canvas、文字用 HTML 叠上去);不苛刻的场合选谁都行(20:553-567)。
  • 练习:形状、饼图标签、弹跳球、预计算镜像(变换会让画位图变慢,可测;几百个角色/几千粒子就不能每帧变换)(20:589-623)。

Ch18 HTTP and Forms(text/21-fm-http-and-forms.txt, 610 段)

  • Berners-Lee 题词:「URL、HTTP、HTML 之外什么都没有;没有中央计算机、没有统一网络、没有运营组织;Web 不是存在于某处的『东西』,而是信息得以存在的『空间』」(21:5)。
  • HTTP 解剖:请求行 GET /18_http.html HTTP/1.1;方法 GET/DELETE/PUT/POST;服务器没义务照办——让陌生网站 DELETE 首页它多半拒绝(21:35);资源未必是文件,可以现场生成(github.com/marijnh 查库生成主页)(21:37);HTTP/2 同概念但更复杂换速度,结果一样,书用 1.1 讲(21:41)。
  • 响应:状态码 2 开头成功、4 开头请求有错(404=找不到)、5 开头服务器有错(21:45-47);headers;没有 Content-Type 浏览器不知道怎么显示(21:49-57);body(21:59)。
  • 表单:GET 法→字段拼成查询串挂在 action URL 后 ?name=Jean&message=Yes%3F;URL 编码 %XX;%3F=问号;「似乎有条不成文规矩:每种格式都得有自己的转义写法」;encodeURIComponent/decodeURIComponent(21:77-88);POST 法→查询串进 body(21:90-96);GET 用于无副作用请求,POST 用于改变服务器状态;浏览器会擅自发 GET 预取(21:98)
  • fetch:返回 promise → Response;服务器返回错误码时照样 resolve;只有网络层面失败才 reject(21:115);相对 URL 规则(21:117);resp.text() 也是 promise——响应头一到就 resolve,正文要等;json()(21:119-126);options:{method/body/headers};DELETE→405 method not allowed;Range: bytes=8-19(21:130-143)。
  • HTTP 沙箱(同源策略):themafia.org 的脚本不许拿你浏览器的身份去请求 mybank.com;服务器可声明 Access-Control-Allow-Origin: * 放行(21:148-154)。
  • 两种建模:RPC(远程过程调用,HTTP 只是载体,写抽象层藏掉)vs 资源+方法(PUT /users/larry 带 JSON 文档);后者能吃到 HTTP 的缓存等特性;「HTTP 的概念设计得很好,值得拿来当服务器接口的设计原则」(21:158-166)
  • HTTPS:先验证服务器身份(浏览器认可的证书颁发机构),再加密整条连接;出过伪造/被盗证书事故,但仍比明文 HTTP 安全得多(21:170-176)。
  • 表单字段:input 类型一览(text/password/checkbox/color/date/radio/file);textarea 要闭合标签;select/option(21:184-234);值变化发 change 事件(21:236)。
  • 焦点:focus()/blur()/document.activeElement;autofetch……autofocus 属性比 JS focus 好:浏览器可以拒绝;tab 键顺序 tabindex(0=可聚焦但不改顺序)(21:256-263);disabled 字段变灰不可改,耗时操作进行中把按钮禁用防连点(21:274)
  • form.elements 既按序号又按 name 取;字段 .form 回指(21:278-295);submit 事件+preventDefault=接管提交:先验证,或用 fetch 发送不刷新页面(21:299-313)
  • 文本域:value 属性;selectionStart/selectionEnd=光标/选区,可写;F2 插入 Khasekhemwy 的例子(21:319-342);change 是「改完失焦」才发;input 是每敲一次发一次——要即时响应用 input(21:344)
  • checkbox.checked;label 包住=点击文字也能切换;radio 同 name 互斥(21:359-393);select multiple;option 的 value 缺省取文本;options+selected(21:399-427)。
  • 文件字段=守门人:脚本无权读你机器上的文件,但你在文件框里选中文件=浏览器视为授权(21:431);files 数组(not 真);name/size(字节=8 位块)/type;没有「内容」属性;FileReader 异步读 readAsText——为 promise 诞生前设计的接口,可包成 promise(21:449-481)
  • localStorage:键→字符串,活得比页面久;不同域名各一个隔间;浏览器限额;例:笔记应用 setState(整体状态)+JSON 存取+?? 缺省;sessionStorage 关浏览器就没(21:485-556)
  • 练习:内容协商(eloquentjavascript.net/author)、JS 工作台、Conway 生命游戏(21:580-610)。

Ch19 Project: A Pixel Art Editor(text/22-fm-project-a-pixel-art-editor.txt, 586 段)

Ch20 Node.js(text/23-fm-node-js.txt, 456 段)

  • Yuan-Ma 题词(老程序员用简单机器也造出好程序;Fu-Tzu:古人用树枝和泥也造出好屋)(23:5)。
  • Node=在浏览器外跑 JS,命令行工具到 HTTP 服务器;需要 Node 18+(23:9-13)。
  • 为什么是 JS:Node 为「让异步编程容易」而生;JS 恰好是少数没有内建输入输出手段的语言——可以无缝套上 Node 那套古怪的网络/文件系统做法而不产生两套不一致的接口;2009 年设计时,浏览器社区已经习惯了回调风格(23:21)
  • node 命令;无参数=交互提示符;process.exit(0)=成功;process.argv 前两项是 node 和脚本名,真参数从下标 2 起(23:47-53);document/prompt 这些浏览器货不存在(23:54)。
  • 模块:.js 默认 CommonJS;.mjs=ES 模块(可用 import/export 不可用 require);以 / ./ ../ 开头按文件解析,node:fs 是内建模块,裸名字去 node_modules 找(23:58-66);reverse 工具例(23:70-88)。
  • NPM:npm install ini→本地 node_modules 目录;装在当前目录而非中央位置——每个应用完全掌控自己的包,版本管理和清理都容易(23:104-106);package.json(name/version/dependencies);npm install 无参数=装 dependencies 列表(23:110-127);npm publish(23:137)。
  • 语义化版本:三段数字;加功能动中段;破坏兼容动首段;^2.3.0=允许 ≥2.3.0 且 <3.0.0(23:133-135)
  • node:fs:readFile 回调风格;utf8 编码;不给编码→返回 Buffer(字节数组)而不是字符串(23:147-160);writeFile 字符串默认按 UTF-8 写(23:170);node:fs/promises=promise 版;xxxSync 变体运行时整个程序停住(23:176-188)
  • node:http:**createServer(每来一个客户端连接回调一次);writeHead/write/end;listen(8000);在监听事件的脚本不会跑到末尾就退出,ctrl-C 关(23:196-219);node:http 自己的 request 很笨重——fetch 在 Node 里也是全局可用的,推荐就用 fetch(23:223)
  • 流(streams):可写流 write/end;可读流靠事件 on("data")/on("end")——适合边到边处理;大写回声服务器例子(23:227-255)
  • 文件服务器实战:methods 对象;handler=async 函数,返回 {body, status=200, type="text/plain"};reject 的错误被 catch 转成 500 响应(23:269-296);body 若是流就 pipe 给 response(23:296)。
  • 安全:urlPath 用 resolve 解析路径后校验必须落在工作目录内(startsWith(baseDirectory+sep)),否则 403 Forbidden;/../secret_file 就是那类攻击(23:298-318)
  • GET:stat 查文件;ENOENT(Unix 风错误码)→404;目录→readdir 列表;文件→createReadStream+mime-types 查 Content-Type(23:334-357);DELETE→204 no content;删不存在的文件也返回成功——HTTP 标准鼓励幂等:同一请求发多次与发一次效果相同(23:377-379);PUT:pipeStream 把 pipe 包成 promise(两端 error→reject,finish→resolve)(23:385-398);curl 演示(23:408-417)。
  • 总结:Node 原为网络任务设计,名字本义就是「网络中的一个节点」;Node 的 I/O 全是异步的,除非你显式用 Sync 变体(23:421-425)。
  • 练习:grep 式搜索工具、MKCOL 建目录(WebDAV)、「网络上的公共空间」(人人可改可涂鸦的网站)(23:429-456)。

Ch21 Project: Skill-Sharing Website(text/24-fm-project-skill-sharing-website.txt, 585 段)

  • Margaret Fuller 题词:「有知识,就让别人在你的蜡烛上点火」(24:5)。
  • 最终项目:演讲分享网站;server(Node)存数据、client(浏览器)展示;talk=presenter/title/summary/comments(24:9-21)。
  • 直播难题:服务器没法主动连客户端 → long polling;「这正是 Node 设计动机之一」(24:21-23);机制=客户端常驻一个请求,服务器没新消息就扣着不答;设最长等待,超时也得答一句(304)防连接死掉;繁忙服务器可能挂着几千个等待请求 → 正好是 Node 的主场(不用一线程一连接)(24:31-37);Fatma/Iman 走查(24:33)。
  • HTTP 接口:JSON 请求/响应;GET /talks、PUT /talks/<标题>(标题要 encodeURIComponent)、DELETE、POST comments(24:43-77)。
  • ETag/If-None-Match 本是缓存机制,被挪用来做长轮询:客户端带 If-None-Match: 4 + Prefer: wait=90,服务器版本号变了才答;304=没变(24:79-98);无访问控制——「互联网上流氓很多,这种系统裸奔上线多半不会有好下场」(24:100)。
  • Router 类:正则路由+捕获组+decodeURIComponent(24:116-136);静态文件交给 NPM 的 serve-static(24:142);handler 链 (request, response, next)(24:174-195)。
  • 输入校验:「外部来的数据都可能是胡说」→ 不合形返回 400(24:226-250);node:stream/consumers 的 json 读请求体(24:232)。
  • 长轮询服务器侧:waiting 数组存 resolve 函数;waitForChanges 挂超时定时器到点回 304;updated() 版本号+1 并唤醒全部等待者(24:307-325)
  • 客户端:state={talks, user};五种 action;setUser 存 localStorage;fetchOK 包装:状态码 ≥400 就 reject(fetch 默认错误码也 resolve)(24:393-398);reportError 用 alert(24:408-410)。
  • 渲染:函数式组件直接返回 DOM;复杂 DOM 这么写很乱 → 业界用模板语言或 JSX(非标准方言,长得像 HTML),都要预处理工具,本章刻意不用(24:458)
  • pollTalks:async 无限循环;失败等 500ms 重试;304 直接下一轮;记 ETag(24:491-515);SkillShareApp talks 变了就全部重画——简单但浪费(24:544);开两个窗口互看=直播效果(24:569)。
  • 练习:磁盘持久化、「重画会抹掉别人正在输入的评论框内容与焦点」(24:575-585)。

附录 Exercise Hints(text/25-fm-exercise-hints.txt, 526 行)

  • 性质=全书练习的提示汇编:「不给出完整答案,只帮你找到答案」(25:5)。按章分节,与各章练习一一对应;多为思路指引(如 FizzBuzz 的「朴素解与聪明解」、reverseArrayInPlace 的对半交换技巧、every 可用德摩根律建在 some 上(25:99-101)、goalOrientedRobot 的缺陷=一次只看一个包裹(25:143-145))。
  • 处理:练习册性质,不逐条拆解;在书卡 notCovered 说明。

前言页(text/01-fm-eloquent-javascript.txt)

  • 4th edition;Marijn Haverbeke;CC BY-NC;书中代码亦可视为 MIT 许可(01:9-13);插图:Péchane 封面、Madalina Tantareanu 章节插图、第 7/16 章像素画 Antonio Perdomo Pastor、第 9 章正则图由 regexper.com 生成、第 16 章游戏概念来自 Thomas Palef(01:15-21);纸质版(No Starch Press)含一章网上没有的 bonus chapter——本 epub 没有(01:23-27)。

Ch11 Asynchronous Programming(text/14-fm-asynchronous-programming.txt, 524 段)

  • 老子题词(14:5)。
  • 处理器很快,网络/磁盘比内存慢得多;单程序等待时别让处理器闲着(14:10-14)。
  • 同步 vs 异步:同步等第二请求=时间相加;同步系统解法=线程;JS 两大平台(浏览器与 Node)选异步不选线程——多线程编程出了名的难,程序同时做几件事时理解难度陡增,公认异步是好事(14:18-34);同步模型的等待是隐式的,异步把等待变成显式的、受你控制的(14:30);异步双向刃:不合直线的程序好写了,合直线的程序反而别扭(14:32)。
  • callbacks:setTimeout、readTextFile;compareFiles 嵌套示例;缩进随异步动作数增长;「异步是传染的」——调用异步函数的函数自己也得异步(14:57-69)。
  • Promise=一张「代表尚未到手的价值」的收据;then 注册回调,已 resolve 再 then 照样调用(14:75);Promise.resolve 包装(14:77-81);构造器传入 resolve(14:83-95);then 本身返回新 promise → 链;randomFile 管道(14:97-112);jsonFile=then(JSON.parse)(14:114-118);心智模型:promise 让代码不用管「值什么时候到」(14:120)。
  • 失败:回调风格约定 (error, value) 第一参表错(14:126-135);promise 有 resolved/rejected 两态;链上任一环失败,整链标记为 rejected,失败点之后的成功处理器全部跳过(14:137);catch 也返回新 promise(14:139-147);管道语义:类型不匹配的处理器被忽略(14:158-170);未处理的 rejection 会被环境报告(14:172)。
  • Carla 乌鸦故事(柏林旧机场偷手机自学编程)(14:174-186);破译 wifi:部分密码响应不同 → 逐位猜(14:190-195);withTimeout:promise 只能 resolve/reject 一次,后到的调用被忽略(14:198-205);递归版 crackPasscode(「你没法在 for 循环里等一个 promise」)(14:209-231);→555555(14:231)。
  • async 函数:伪同步写法;重写后双层循环结构显形(14:238-265);async 函数隐式返回 promise,体里 throw=promise rejected;可在任何 await 处冻结、稍后恢复(14:267-273)。
  • generators:function* + yield;powers(3) → 3/9/27;生成器自动保存局部状态,yield 只能写在生成器自己体内(14:277-307);「async 函数是一种特殊的生成器」(14:309)。
  • 乌鸦艺术项目:9 块 LED 屏各 50×30,每帧 1500 个亮度数字;request 假函数(14:313-352);Promise.all=把一组 promise 合成一个,等全部完成(14:356-365);VideoPlayer(async play + stop)(14:369-402);整整一周,每天夜里大屏幕上出现巨大的发光橙鸟(14:404)。
  • 事件循环:主脚本与回调各自一口气跑完;异步行为发生在自己空空的调用栈上——这就是跨异步代码管异常这么难的原因:回调抛异常时你的 catch 不在栈上(14:408-421);环境同一时刻只跑一段程序:大循环+事件队列;慢代码会拖延其他事件;setTimeout 20ms 实际 55ms 才跑(14:423-434);已 resolve 的 promise 也要等当前脚本跑完才调回调:"Me first!" 先于 "Done"(14:436-441)。
  • 异步 bug:fileSizes 在 await 两侧用 += ——每个 += 都从空串出发,最终只剩耗时最长的文件一行;修法=map 返回行、Promise.all 后 join(照例:算新值比改旧值不易错)(14:449-480);显式异步的好处:缺口容易看见(14:482)。
  • 练习:Quiet Times、Real Promises、自己实现 Promise_all(14:490-524)。

Ch12 Project: A Programming Language(text/15-fm-project-a-programming-language.txt, 391 段)

  • Abelson/Sussman 题词:「求值器不过是另一个程序」(15:5)。
  • 造语言意外地容易(只要目标别定太高)且启发极大;没有魔法(15:9-13);Egg:小而简单,但强大到能表达任何计算(15:13)。
  • Parsing:parser=文本→反映程序结构的数据结构;Egg 里一切都是表达式(名字/数字/字符串/应用)(15:17-23);do(define(x,10), if(>(x,5),…))(15:25-30);JS 的运算符 > 在 Egg 里就是普通 binding(15:30)。
  • 表达式对象三型:value/word/apply(15:32-45);syntax tree(15:47);INI 解析器按行处理,这里表达式有递归结构 → 解析器也要递归(15:49-53);parseExpression+parseApply 互递归;multiplier(2)(1) 连续调用 → parseApply 解析完还要再看一眼有没有下一个括号(15:55-110)。
  • Evaluator:evaluate(expr, scope);specialForms 优先于普通调用(15:132-164);为什么 if 必须是特殊形式:普通函数的实参在调用前全部求值,而 if 只该求值两个分支之一(15:190);Egg 的 if 只有 false 算假(0/空串不算,与 JS 不同)(15:188);while/do/define(define 返回被赋的值)(15:194-226)。
  • 环境:topScope 放 true/false;用 Function 在循环里合成一批运算符函数(15:245-249);用原型链表示嵌套作用域:run 时 Object.create(topScope)(15:260-264);Egg 版 1-10 求和 → 55;「比 JS 丑,但对一个不到 150 行代码实现的语言不算坏」(15:266-276)。
  • fun 特殊形式:最后一个参数=函数体,其余=参数名;局部作用域=Object.create(scope);plusOne(10)→11;递归 pow(2,10)→1024(15:280-321)。
  • Compilation:现在造的是解释器;编译=在解析和运行之间加一道变换,把能提前做的提前做(比如绑定不再按名字查);传统编译到机器码,但任何把程序换一种表示的过程都算编译(15:323-333);可以把 Egg 编译成 JS 用 Function 跑(15:331)。
  • Cheating:if/while 就是 JS 自家 if/while 的薄包装;Egg 的值就是 JS 的值(15:335-337);小语言能干真活:比如自己写正则的解析器求值器;或「用逻辑描述语言→编译成解析器」的记法(15:341-353);DSL 定义:为狭窄领域量身定做,因只描述该描述的东西而更有表达力(15:355)。
  • 练习:数组支持、closure(f(4)(5)→9,解释哪个机制让它成立)、skipSpace 顺带跳注释、set 特殊形式(define 兼任赋值的歧义问题)(15:357-391)。

Ch13 JavaScript and the Browser(text/16-fm-javascript-and-the-browser.txt)

(读后补)

Ch5 Higher-Order Functions(text/08-fm-higher-order-functions.txt, 428 段)

  • Hoare 题词:设计要么简单到显然没有缺陷,要么复杂到没有明显的缺陷(8:5)。
  • 开篇论证:sum(range(1,10)) 一行 vs 六行循环——把 sum/range 的定义算进去第二版更大,但更可能对,因为词汇表与问题对应:求和不是关于循环和计数器,是关于范围与和(8:11-30)。
  • abstraction=编程里的「词汇」;豌豆汤两份菜谱(逐步刀切 vs soak/simmer/chop);有用技能:察觉自己在过低的抽象层干活(8:34-50)。
  • 抽象重复:repeat(n, action) 把「动作」当函数值传入(8:56-90)。
  • higher-order function 定义:以函数为参数或返回函数;来自数学,数学里函数与其他值的区分更严肃(8:96);抽象「动作」而非只抽象值(8:98);三种形态:造函数 greaterThan(n)→m=>m>n(8:100-105)、改函数 noisy(f) 包一层日志(8:109-119)、提供新控制流 unless(test,then)(8:123-134);forEach(8:135-139)。
  • SCRIPTS 数据集:Unicode 140 种文字,81 活 59 死(8:145);Coptic 对象:ranges(半开区间)/direction(ltr/rtl/ttb)/year=-200/living/link(8:151-162)。
  • filter:建新数组不改原数组,纯(8:168-185);map:等长变换(8:196-208)。
  • reduce(=fold):组合函数+起始值;reduce([1,2,3,4],(a,b)=>a+b,0)→10(8:218-227);标准方法可省 start 取首元素(8:229-232);characterCount 用 reduce+参数解构求字符数(8:236-247);Han 9 万+字符最大;Han unification(中日韩共用一块字符省码位)至今让一些人大怒(8:249)。
  • 可组合性:不用高阶函数找最大脚本的循环版并不糟,只长 4 行(8:255-265);活的文字平均起源年 1165、死的 204;管道读法:filter→map→average→round(8:269-280);大循环版更难看出算了什么、中间结果不成值、average 难抽出(8:284-294);第一版真的会建中间数组,做的事更多;通常读得懂优先,大数据反复处理才值得换速度(8:296)。
  • characterScript 用 some(8:304-318)。
  • UTF-16 批判:字符串是 16 位 code units 序列;UTF-16「今天被普遍认为是坏主意」「几乎像故意招引错误」;emoji 普及让大家都用上双单元字符,坑的负担被公平分摊(8:322-324);🐴👟 length→4(8:328-337);charCodeAt 给半字符,codePointAt 给完整码点但下标仍是 code unit 序号(8:339);for-of 给真字符(8:341-348)。
  • countBy 分组计数(8:356-375);find 返回第一个通过的元素,没有则 undefined(8:375)。
  • textScripts:'英国的狗说 "woof", 俄罗斯的狗说 "тяв"' → 61% Han, 22% Latin, 17% Cyrillic(8:379-394)。
  • 练习:flatten、自己的 loop、every(=数组版 &&,some=数组版 ||)、dominant writing direction(8:406-428)。