一等函数(上)— 定义、回调与参数的全部花样
一等函数(上)— 定义、回调与参数的全部花样
这一章讲三件事: 定义一个函数居然有六种写法,而且行为不完全一样; 「函数是值」这件事能玩出什么花样(存进集合、挂上数据、包上缓存); 以及参数与实参这对「听起来是一个词」的概念,加上两个 ES2015 新参数语法和 TypeScript 的函数类型。 这是全书最重要的一章——它立的地基,后面三章全在上面盖。
1. 为什么一上来先讲函数,而不是对象
大多数讲 JavaScript 的书从对象讲起,这本书偏不。作者的理由一句话:
普通开发者和忍者写 JavaScript 的区别,在于是否把它当作一门函数式语言来理解。1
「函数式语言」在这里不是高深的学术标签,而是一个可操作的事实: 在 JavaScript 里,函数是一等对象(first-class object)——这个术语的意思是, 函数和其他任何对象享有完全相同的待遇,可以:2
- 用字面量直接写出来(就像写
3或"hello"一样); - 赋给变量、数组元素、别的对象的属性;
- 当参数传给别的函数;
- 被别的函数返回;
- 自己挂上属性和方法。
四条对照,对象是啥样、函数就是啥样——唯一多出来的能力,是函数可以被调用。 这一句话是全书的总开关。本章其余所有内容,都是「函数是个值」的推论。
主线例子:一个「忍者行动登记簿」
为了让每个机制都有落点,我们把全章串在一条主线上——一个不断进化的函数
performAction(ninja, action),它登记「谁做了什么」。旅程分六站:
① 定义它:声明式 vs 表达式 vs 箭头式 —— 写法不同,行为有别(§2)
② 用它当值:塞进 Set、挂上元数据、包上缓存(§4)
③ 给它传参数:多传了、少传了,各会发生什么(§5)
④ 让它收「任意多个」参数:rest 参数(§5)
⑤ 让参数有默认值:不传也能用(§5)
⑥ 让 TypeScript 替你把关:类型、重载、文档注释(§6)2. 定义函数的六种方式
原书列了六份清单,我们逐份过,重点看「行为差异」而不只是语法。3
2.1 函数声明:老派而特权
function myFun() { return 1; }function 关键字 + 名字 + 参数表 + 花括号体。它的两项特权:名字必填;
可以当一条独立语句存在。第二项特权有个重要推论(第 6 章揭晓):函数声明会被
「提升」到作用域顶部,所以在它定义之前就能调用。现在只要记住「声明是独立语句」,
「提升」一词先欠着,第 07 章兑现。4
函数声明还可以嵌套——函数里定义函数,在 JavaScript 里完全合法且常见:
function ninja() {
function hiddenNinja() { return "ninja here"; }
return hiddenNinja();
}这在 C 家族语言里是异端,在 JavaScript 里是日常——它是「函数是值」的直接体现, 也是第 7 章闭包的伏笔。
2.2 函数表达式:函数是值,所以能出现在任何「表达式该在」的地方
const myFun = function (a) { return a; };函数表达式总是别的语句的一部分——赋值右边、调用参数、返回值。 反过来,声明必须独立成句;这是两种写法的语法分水岭。5
看一下原书那段著名的「一处六种用法」代码,它值得整段记住:6
function myFunctionDeclaration() { function innerFunction() {} } // 声明(含内部声明)
const myFunc = function () {}; // 表达式:赋值右边
myFunc(function () { // 表达式:调用参数
return function () {}; // 表达式:返回值
});
(function namedFunctionExpression() {})(); // 命名表达式,立即调用
+function () {}(); // 四个变体:用一元运算符
-function () {}(); // +、-、!、~ 告诉解析器
!function () {}(); // 「这里是表达式,不是声明」
~function () {}(); //名字上,表达式可以匿名(上面大多匿名),这是它和声明的第二处差异—— 声明必须有名字是因为只能靠名字调用;表达式活在别的语句里,自然有别的调用渠道: 赋值给变量就用变量调用,当参数就在接收方内部用参数名调用。
2.3 IIFE:定义完当场调用的表达式
上面那组怪写法有个专门名字:立即调用函数表达式(Immediately Invoked Function Expression, IIFE,读作「iffy」)。它的意义值得从原理上理解一次:7
调用一个函数的语法是「一个求值为函数的表达式 + 一对括号」。平时括号前是个名字(ninja()),
但任何求值为函数的表达式都合法——包括一个当场写出的函数表达式。
于是 (function(){...})() 就是「造一个函数,立刻调它」。
为什么外面非要包一层括号? 纯语法原因,值得记牢:
JavaScript 解析器看到以 function 开头的一句,就铁了心按「函数声明」解析,
而声明必须有名字——function(){}(3) 会语法错误;function f(){}(3) 更阴险,
会被解析成两条语句(声明 function f(){} + 一个孤零零的 3),静默地什么都不做。
包上括号,解析器改按「表达式」解析,后面的 (3) 才被当成调用。
一元运算符变体(+function(){}() 等)是同一个目的的不同写法——运算符逼解析器进入表达式模式,
运算结果没人要也无所谓。8
IIFE 在历史上的大用途是造「私有作用域」(第 7 章),今天它更多出现在: 一次性初始化、不想污染命名空间的临时计算。
2.4 箭头函数:为一行函数而生的语法糖
ES2015 加入。对比同一个函数:9
const hypotenuse = function (a, b) { return Math.sqrt(a ** 2 + b ** 2); };
const hypotenuse = (a, b) => Math.sqrt(a ** 2 + b ** 2); // 等价,一行搞定省掉了 function、花括号、return。语法有两档:10
(参数) => 表达式 // 简式:表达式的值就是返回值
(参数) => { 语句; return …; } // 块式:和普通函数一样,没写 return 就返回 undefined参数表有一条小规矩:零个或多个参数必须带括号,恰一个参数可以省括号(name => ...)。
=> 在规范里的正式名字就叫「箭头函数运算符」——它是为箭头函数发明的,别处不用。
箭头函数和普通函数有一个行为差异,本章先按下不表(第 6 章主角:this 的处理完全不同)。11
2.5 剩下的三种:方法简写、类字段、Function 构造器
三种快览,原书也是这个态度:12
const myObj = { myFun(key) { return this[key]; } }; // 方法简写:对象/类里的省 function 写法
class MyClass { myFun = () => this.x * this.y; } // 类字段(ES2022):= 号定义,实为箭头函数
const f = new Function("a", "b", "return a + b"); // Function 构造器:字符串拼函数,少用方法简写行为同函数表达式;类字段是 ES2022 的新语法,行为同箭头函数(第 6、9 章展开);
new Function 与 eval 同族——把字符串当代码在运行时解析,安全上必须谨慎,知道存在即可。
还有一族特殊函数本章不展开:生成器(generator),定义带个 * 号(如 function*),
第 8 章整章讲。13
3. 函数是一等对象:四个对照实验
书把「函数 == 对象」做成了一组对照,值得原样走一遍。对象能做的四件事,函数逐件复刻:14
| 对象能做的 | 对象的写法 | 函数的同款写法 |
|---|---|---|
| 赋给变量/数组元素/属性 | const ninja = {}; ninjaArray.push({}); ninja.data = {}; | const f = function(){}; arr.push(function(){}); ninja.data = function(){}; |
| 当参数传 | hide({}); | call(function(){}); |
| 从函数返回 | return {}; | return function(){}; |
| 动态挂属性 | ninja.name = "Hanzo"; | f.ninja = "Hanzo"; |
表格的最后一行是伏笔——「函数能挂属性」看着不起眼,第 4 节和第 6 章都要用到它。
函数式编程(functional programming) 的名字就来自这里:既然函数是值, 就可以用函数的组合来解决问题,而不是一步步指挥计算机(命令式编程)。 组合出来的代码更容易测试、扩展和模块化——这是原书给的方向性评价,不是本章要展开的技能。15
3.1 回调:一等对象的第一份工作
回调(callback) 的定义朴素:「把一个函数交给别的代码,约定在以后的某个合适时刻,
由它回头调用(call back)这个函数」。16 之前章节已经见过它的两个雇主:
浏览器的事件系统(addEventListener("keydown", 处理器))和 Node 的文件系统。
书里最剥光衣服的版本是一个「完全无用的函数」,专门用来显露机制:
function useless(ninjaCallback) {
return ninjaCallback(); // 把传进来的函数当场调用
}
console.assert(useless(getText) === text, "The useless function works!");useless 收一个函数、调一个函数——机制上,getText 是通过参数进入 useless 的,
在内部以参数名 ninjaCallback 被「回调」。运行它的输出顺序(书里给了完整日志):
Before defining functions ← 定义阶段不执行任何函数体
Before making all the calls ← 还是只定义
In useless function ← 进入 useless
In getText function ← 回调发生,getText 被执行
After the calls have been made这个顺序本身就在教学:函数定义不产生动作,动作全部发生在调用点。17
回调还有个纯便利的用法值得单独讲——排序的比较器。数组的 sort 方法接受一个比较函数:
排序算法每次要比较两个元素,就回头调这个函数,让它裁决先后:18
const values = [0, 3, 2, 5, 7, 4, 8, 1];
values.sort((value1, value2) => value1 - value2);
// 回调约定:返回负数 → a 在前;正数 → a 在后;0 → 平手这里没有任何排序算法出现在你的代码里——底层用哪个算法是运行时的事,
你只提供了「谁该排前面」这条裁判规则。这就是回调的威力分工:
通用机制留给了系统,应用相关的判断交给了你。(一个坑:不给 sort 传比较器,
它默认把元素转成字符串按字典序排——数字 10 会排到 2 前面!)19
4. 函数玩的三个花样
这一节回答:「一等对象」除了回调,还能干嘛?书里给了三个越来越实用的花样。
4.1 存进集合:用 Set 去重
场景:管理一批回调(比如同一事件要通知的所有处理器),要求同一个函数只被加一次—— 同一个事件触发时,同一个处理器不该被连调两遍。20
笨办法是数组 + 每次添加前循环查重,书里明确嫌弃:「as a ninja, we want to make things work well, not merely work」。因为函数是对象,而对象是引用比较的, 可以直接放进 Set——JavaScript 内置的「保证元素唯一」的集合:
const funcSet = new Set();
const increment = () => { count++; };
funcSet.add(increment);
funcSet.add(increment); // 第二次加同一份引用:无效操作
funcSet.forEach((func) => func()); // 每个函数恰好被调一次,count === 1 ✓唯一性的精确含义:引用相等(reference equality)。
第二个 add(increment) 无效,是因为加的是同一个引用;
两个函数就算函数体一字不差,也是两个不同对象:
funcSet.add(() => { count++; }); // 加进去了,新引用
funcSet.add(() => { count++; }); // 又加进去了,还是新引用「函数体相同 ≠ 同一个函数」这个区分,在第 11 章 Map 的键比较时还会回来。21
4.2 挂元数据:给函数贴标签
函数既然能挂属性,就可以挂元数据(metadata,描述数据的数据):
function func() {}
func.str = "a string";
console.assert(func.str === "a string", "We can attach data to functions");原书给了一个克制而真实的用例:React 里给组件函数挂 displayName,
调试工具里就能显示组件名——因为函数名在**代码压缩(minification)时通常被毁,
而手工挂的属性会被保留。顺带一句 func.name:函数声明时的名字,
在调用栈(stack trace)**里看到的就是它。22
但原书立刻泼了冷水:能挂 ≠ 该挂。函数上挂数据,等于让摸到这个函数的所有代码 都能读写它,破坏了封装(encapsulation)——数据只该被需要它的模块看见。 元数据(「描述这个函数的信息」)是少数正当用途;业务数据挂函数上,通常是懒。23
4.3 memoize:给函数包一层「记住答案」的壳
memoization(书特意注明:没拼错,不是 memorization)指: 造一个会记住自己算过的结果的函数——同样的输入再来,直接返回缓存的答案, 跳过计算。24
主线走一遍 isPrime(判素数)的实现和被 memoize 的过程,这是本章的主线例子,
值得带数走一遍:
const isPrime = (value) => {
if (value === 0 || value === 1) return false;
for (let i = 2; i < value; i++) { // 从 2 试除到 value-1
if (value % i === 0) return false; // 有因子 → 不是素数
}
return true;
};这个算法的成本随输入增大:判 5 只要试 2、3、4 三次除法;判 997 要试 995 次。 如果同一个值会被反复查询,缓存就是白捡的优化。memoize 包装器:25
function memoize(func) {
const cache = new Map(); // 每个被 memoize 的函数,都有自己的 cache
return (arg) => { // 返回一个新函数:壳
if (!cache.has(arg)) { // 第一次见这个输入?
cache.set(arg, func(arg)); // 真算一次,存起来
}
return cache.get(arg); // 之后直接取
};
}
const memoizedIsPrime = memoize(isPrime); // isPrime 原函数,原样可用带数走一遍 memoizedIsPrime(5):
| 步骤 | cache 的状态 | 动作 |
|---|---|---|
调用 memoizedIsPrime(5),cache 里没有 5 | {} | 真调 isPrime(5) → 试除 2、3、4,无因子 → true |
| 存入缓存 | {5 → true} | 返回 true |
再调 memoizedIsPrime(5) | {5 → true} | 不再计算,直接返回 true |
调 memoizedIsPrime(7) | {5 → true} | 真调 isPrime(7) → 试除 2–6 → true,存入 |
这个壳的三个设计点都是「函数是一等对象」的果实:
- 包装而不改原函数:
memoizedIsPrime是新函数,isPrime原样保留——想不用缓存,用原版; - 缓存封装在闭包里:
cache是memoize体内的局部变量,只有返回的壳能摸到—— 这是第 5 章第一次、全书无数次用上闭包的地方; - 包装器可复用:
memoize(任何单参函数)都能用,不止 isPrime。
书里也照实给了限制:这个壳只支持单参数函数(多参数得先序列化键——把几个参数拼成一个能当键的值——才能缓存,复杂);
lodash 库有更完善的版本,memoize-one 库只缓存最近一次结果以省内存。26
5. 参数与实参:两个词,四套规则
先把一对孪生词掰开(原书特意正式定义):27
- 参数(parameter):函数定义时列出的变量,是占位符;
- 实参(argument):函数调用时传进去的值,是真身。
function skulk(ninja) 里的 ninja 是参数;skulk("Hattori") 里的 "Hattori" 是实参。
一个活在定义里,一个活在调用里。
5.1 个数不齐,会发生什么
JavaScript 对「个数不一致」完全宽容——不报错,按规矩消化:28
- 实参多于参数:多出来的没人接,被丢弃。
practice("Yoshi","sword","影","katana")若定义只有三个参数,第四个"katana"落地即消失(注意:不是报错,是静默丢弃); - 实参少于参数:没接到值的参数 =
undefined。practice("Yoshi"), 后两个参数都是undefined。
这套规则的日常后果:函数无法靠签名强制调用方传够参数——传错、传少,运行时要么拿 undefined
一路酿祸,要么靠函数体内的默认值兜底。这正是下一小节和 TypeScript 登场的理由。
5.2 rest 参数:「剩下的都给我」
想让函数收任意多个实参?ES2015 之前要写 workaround(书里明说不教了,
arguments 对象整族退场);现在把最后一个参数前面加 ...:
function multiMax(first, ...remainingNumbers) {
const sorted = remainingNumbers.sort((a, b) => b - a); // 降序
return first * sorted[0]; // 第一个 × 剩余里最大的
}
console.assert(multiMax(3, 1, 2, 3) == 9, "3*3=9"); // 3 × max(1,2,3)带数走一遍 multiMax(3, 1, 2, 3):第一个实参 3 给 first;... 把剩下的 [1, 2, 3]
收成一个真数组 remainingNumbers;降序排成 [3, 2, 1],取 sorted[0] 得 3;返回 3 × 3 = 9。29
规则只有一条硬的:rest 参数必须是参数表的最后一个——不然「剩下的」无法定义,
直接 SyntaxError: parameter after rest parameter。
rest(...)和展开(spread,第 6 章细用)是同一个符号的两个方向:
rest 在定义处收拢,展开在调用处散开。
5.3 默认参数:不传,就用这个
主线例子回到 performAction(ninja, action)。多数忍者都在潜行(skulking),
只有一个例外——老派写法要在函数体内判断:
function performAction(ninja, action) {
action = action === undefined ? "skulking" : action; // 手写的兜底
return ninja + " " + action;
}ES2015 把这件事收进签名里:
function performAction(ninja, action = "skulking") { // 缺省值直接写在参数表
return ninja + " " + action;
}
performAction("Fuma"); // "Fuma skulking" —— 用默认值
performAction("Yagyu", "sneaking"); // "Yagyu sneaking" —— 显式值覆盖默认默认值在什么时候生效? 实参是 undefined 时——包括「没传」和「显式传了 undefined」两种。
所以 performAction("Yoshi", undefined) 同样得到 "Yoshi skulking"。30
默认值可以是任意表达式,甚至引用前面的参数:
function performAction(ninja, action = "skulking", message = ninja + " " + action) { ... }后一个参数拿前两个拼默认值——合法,但原书给这朵花的判语是「urge caution」: 可读性在受损,能不用就不用。31
6. TypeScript 视角:让坏调用死在编译期
纯 JavaScript 的世界里,任何函数可以被任何实参以任何个数调用;
想让参数类型对,得在运行时自己查(书里给了 typeof x !== "number" 就 throw 的样板)。
TypeScript 把这道防线挪到编译期,而且不产生任何运行时代码。
对照两版 doubleNumber:32
// JavaScript:运行时手工查
const doubleNumber = (x) => {
if (typeof x !== "number") throw new Error(`Expected x to be a number, but received type ${typeof x}.`);
return x * 2;
};
doubleNumber("abc"); // 运行时才炸
// TypeScript:编译期查,x 后面的 : number 就是类型注解
const doubleNumber = (x: number) => x * 2;
doubleNumber("abc"); // 编译期就报:类型 'string' 不能赋给 'number'编译器把 : number 剥掉就完事——没有运行时检查,没有体积代价。
原书强调,这种「零成本」正是 TypeScript 成功的大原因。33
6.1 参数类型与推断的协作
TypeScript 认出 rest 与默认值,而且会从默认值反推参数类型:
function multiMax(first: number, ...remainingNumbers: number[]) { ... } // 数组类型注解
function performAction(ninja: string, action = "skulking") { ... } // action 推断为 string「action 推断为 string」的依据:默认值是字符串字面量。于是
performAction("Yoshi", undefined) 也通过检查(undefined 触发默认值,最终仍是 string)。
想允许「string 或 null」就显式写联合:action: string | null = "skulking"。34
6.2 返回值:可写可不写,写多有好处
参数类型在 strict 下基本必填,返回值却可以省——检查器把函数体内所有 return 的类型
合起来推断。capitalize("seattle") 的返回类型被推断为 string,编辑器里悬停可见完整签名:
const capitalize: (str: string) => string。35
但「可省」不等于「省了更好」。原书的反例是一个符号函数:
const numberSign = (num: number): "positive" | "negative" => {
if (num > 0) return "positive";
else if (num < 0) return "negative";
} // 报错:函数可能落到末尾、返回 undefined,而声明的返回类型不含 undefined显式写了返回类型 "positive" | "negative",检查器替你抓到一个漏了零的情况;
补上 "zero" 和对应的 return 才过。36 这条规律值得记住,原书把它推广成一句:
声明的类型越多,检查器能帮到的地方越多。
6.3 函数重载:一份实现,多张签名
需求:一个 compare 函数,既能比两个字符串、又能比两个数字,但绝不能一个字符串配一个数字。
单签名做不到精确(只能标成 string | number 组合,四种配对全放行),
解法是函数重载(overloading):实现之前,先列几张「合法调用」的类型签名:37
function compare(a: string, b: string): number; // 签名①:两个字符串
function compare(a: number, b: number): number; // 签名②:两个数字
function compare(a: string | number, b: string | number) { // 实现:宽类型,管运行时
if (typeof a === "string") return a.localeCompare(b as string);
if (a === b) return 0;
return a < (b as number) ? -1 : 1;
}检查器只认前面那几张签名:compare("a", 3) 和 compare(3, "a") 编译期直接拒;
实现里的宽类型 + as 断言,是告诉检查器「能走到这,类型必然匹配」。
箭头函数没有重载语法,但可以给函数类型起名字,达到同样效果(书中有样板,机制相同)。38
和 C++ 的重载划清界限(原书特别说明):C++ 里每个签名有各自的实现; TypeScript 的重载签名全部消失于编译,运行的是同一个实现—— TypeScript 的哲学始终是「不替你加任何代码」。39
6.4 TSDoc:写给工具的注释
类型注解服务检查器;TSDoc 服务人类——一种带 @ 标记的注释格式,
TypeScript 感知的工具(编辑器、文档生成器)都能读:
/**
* 输出一个描述忍者行动的字符串。
* @param ninja — 忍者的名字
* @param action — 忍者在做的动作;null 表示什么都没做(默认 "skulking")
* @returns 形如 "Yagyu sneaking" 的字符串
*/
function performAction(ninja: string, action: string | null = "skulking") { ... }@param 标参数、@returns 标返回值,其余自由文本描述整体。
妙处在打包:编译器生成 .d.ts(类型声明文件)时,会把 TSDoc 注释一起带上——
于是用你包的人,在编辑器里悬停就能看到文档。写好 TSDoc,是给别人用的 API 的基本礼仪。40
7. 作者的判断与证据
书内可验的机制(都带 assert,可亲手跑):
- IIFE 的括号是语法必需(去掉即解析错误),机制可由报错信息直接验证;41
- memoize 的缓存封装在闭包里、外部不可达,由代码结构直接可见;42
- 默认值触发条件是「实参 === undefined」,
performAction("Yoshi", undefined)也通过检查的事实, 在 TypeScript 节有明确陈述。43
作者立场(经验之谈):
- 「函数式取向是忍者与普通开发者的分水岭」是全书的纲领性立场,本章是宣言处,不是论证处;
- 「默认参数里引用前面的参数 urge caution」「元数据该挂才挂」是风格判断,不同团队口径可以不同;
- 「函数重载签名只是类型层的多态」是对 TypeScript 哲学的准确转述,但「实现里需要
as断言」 是 TypeScript 5.5 时点的能力边界,书里注明了版本——未来版本的检查器可能 smarter。
8. 边界与局限
- arguments 对象整族删除。 旧版靠它收任意实参,ES2015 的 rest 参数取代之;
书里明说不再使用,只在边栏指了 MDN。读老代码遇到
arguments知道它是前辈即可。 - this 差异按下不表。 箭头函数与 function 函数在 this 上的行为差是第 6 章的主菜; 本章只给了语法。
- 重载、TSDoc 是 TypeScript 特性,不是 JavaScript。 运行时一切照旧——这是特性不是 bug, 但「类型检查过了」永远不等于「逻辑对了」。
- MEAP 缺口: 函数式编程的系统展开(第 11 章预告)、自定义 ESLint 规则(第 15 章预告) 都不在本版。
9. 可带走的(合上这一章,你应该有这十条)
- 一等对象四条:能字面量写出、能赋给变量/属性、能当参数、能当返回值——函数全占,外加「可被调用」。
- 声明 vs 表达式:声明必须独立成句、名字必填、可被提升;表达式是别的语句的一部分、可匿名。
见到以
function开头的语句,解析器铁定按声明读。 - IIFE = 「造函数立刻调」;外层的括号(或
+!等一元运算符)是逼解析器按表达式读的语法手段。 - 箭头函数两档语法:简式直接返回表达式,块式要自己写 return;单参数可省括号。
- 回调 = 把函数交给别人回头调;排序比较器是它的标本:算法归系统,裁决归你。
- 函数进 Set 按引用去重;函数体相同 ≠ 同一函数。挂属性可以,但想想封装——元数据是少数正当用途。
- memoize = 闭包包一个 Map:真算一次、缓存结果、壳可复用、原函数不动。
- 参数是定义里的占位符,实参是调用时的真身;多传丢弃、少传补 undefined,永不报错。
- rest 收拢(
...rest在定义处)、默认兜底(action = "skulking");rest 必须是最后一个参数。 - TypeScript 把调用检查挪到编译期:类型注解零运行时成本、返回值可省但写上能抓漏分支、 重载用多张签名挡掉非法组合、TSDoc 注释随 .d.ts 一起发给使用方。
11. 深读一:六种定义方式,逐个走完整行为
第 2 节给了六种的清单,这里把每一种的「语法长什么样、行为上记住什么」钉成一张总表, 再对最容易混的前三种做逐字段拆解。
| 方式 | 语法核心 | 名字 | 能提前调用 | this 规则 | 备注 |
|---|---|---|---|---|---|
| 函数声明 | function f(){} | 必填 | 能(提升) | 看调用方式 | 必须独立成句 |
| 函数表达式 | function(){} 出现在别的语句里 | 可省 | 不能 | 看调用方式 | 随写随用 |
| 箭头函数 | (a, b) => … | (无名字位) | 不能 | 定义处外层 | 不能当构造器 |
| 方法简写 | 对象/类体内 m(){} | 同表达式 | 不能 | 看调用方式 | 等价于函数表达式 |
| 类字段 | 类体内 m = () => … | — | 不能 | 所在实例 | ES2022;特殊箭头函数 |
| Function 构造器 | new Function("a","b","return a+b") | — | — | — | 字符串拼代码,慎用 |
这个表本身值得收藏:查「这段函数的行为」时,先问它是哪一行定义出来的。21
11.1 声明的语法字段,逐字段过
书里的图 5.1 把声明拆成五段,我们照拆:
function 必需的关键字,声明的开场白
myFun 必需的名字——声明只能靠名字被调,没有名字就没有存在意义
(a, b) 参数表:逗号分隔的名字,可为空,但括号必填
{ … } 函数体:语句的列表,可以为空,但花括号必填「必须独立成句」再强调一次它的反面:声明不能出现在表达式的位置上。
const f = function g(){}; 里右边的 function g(){} 是表达式(赋值给 f);
而 function g(){} 单独占一行,是声明。同一个写法、两种身份,位置定身份。22
11.2 表达式的「名字可选」,可选的理由
表达式为什么可以匿名?因为它不需要名字也能被调到——书里给了两条通道,都值得记住:23
const doNothing = function () {}; // 通道一:赋值给变量,用变量调
doNothing();
function doSomething(action) { action(); }
doSomething(function () {}); // 通道二:当参数传,在接收方内部用参数名调匿名还有个工程上的好处(书里点了一句):一个只在一处用的函数,起名字反而是噪音—— 名字会留在模块的命名空间里,后来者还得搞清楚它没被别处引用才敢删。 「就地定义、就地使用」让代码的读者少记一个名字。
11.3 IIFE 的四个一元运算符变体,逐个解释
+function(){}()、-function(){}()、!function(){}()、~function(){}()——
四个符号各是什么?它们都是 JavaScript 的一元运算符(只对一个值运算):
正号、负号、逻辑非、按位取反。放在函数表达式前面,作用是同一个:
让解析器把这个 function 读成表达式而不是声明。运算本身的结果(比如 ! 出来是 false)
没人接、没人要——「from a computational perspective, only the calls to our IIFEs matter」。
选哪个纯属库作者口味,读懂的机制就是「一元运算符 = 表达式模式开关」。24
11.4 箭头函数两档语法,边界情形清单
书里图 5.3 之外,把两档语法的边界情形列全(都来自原书的语法说明):25
() => 表达式 // 零个参数:括号必填
(a, b) => 表达式 // 多个参数:括号必填
a => 表达式 // 恰一个参数:括号可省(可省,不是必须省)
() => { 语句 } // 块式:和普通函数一样,return 要自己写三个高频误会,一并清掉:
- 简式的值就是表达式的值——
name => "Greetings " + name返回拼接结果, 因为箭头后面是表达式,不是语句块; - 块式不写 return 就返回 undefined——
name => { "Greetings " + name; }是常见手滑, 花括号里那句话只是没人接的表达式; =>别处不用:规范里它的正式名字是「箭头函数运算符」,是专门为箭头函数造的符号。
12. 深读二:回调的完整解剖,从 useless 到排序
第 3 节的 useless 例子走了一遍执行顺序,这里把「回调的分类学」补齐—— 回调之间的差别不在语法,在谁、在什么时候、为什么调用它。
12.1 三种回调,按「雇主」分类
| 雇主 | 例子 | 调用的时机 |
|---|---|---|
| 你自己的代码 | useless(getText) | 你决定的任意时刻 |
| 宿主环境(浏览器/Node) | addEventListener("mousemove", fn) | 事件发生的那一刻 |
| 数据结构的算法 | values.sort(compare) | 算法每次需要裁决时 |
三类的共同点是「控制反转」:你的函数把自己的一段逻辑交出去, 由别人决定何时执行。第 8 章的 promise 时代替的是第二类(事件), 第 10 章的数组方法把第三类发扬光大。26
12.2 排序回调的契约,再精确一次
sort 回调的三值契约(负数/正数/零)在第 3 节给过了,这里补两个实操推论:
const values = [0, 3, 2, 5, 7, 4, 8, 1];
values.sort((value1, value2) => value1 - value2); // 升序
values.sort((value1, value2) => value2 - value1); // 降序:把被减数换个位置- 两数相减天然满足契约:小减大得负(前者排前)、大减小得正(前者排后)、相等得零;
- 字符串排序没有减法可用,要借助
localeCompare(第 10 章的 Intl.Collator 是它的工业级版本)。
以及书里那个必须记住的默认行为:不传回调,sort 把元素转成字符串按字典序比较——
[1, 10, 2].sort() 得 ["1", "10", "2"] 的排序结果,即 [1, 10, 2](10 夹在 1 和 2 中间)。
对字符串数组,默认比较器也只按 ASCII 思路排,用户期待的本地顺序要用 localeCompare。27
13. 深读三:三个花样,各自的完整代码与边界
13.1 Set 去重,完整走一遍计数
第 4 节的 Set 例子补全成可运行的完整剧本:28
let count = 0;
const increment = () => { count++; };
const funcSet = new Set();
funcSet.add(increment); // 第一次:加进去了,set 大小 1
funcSet.add(increment); // 第二次:同一个引用,无效操作,大小还是 1
funcSet.add(() => { count++; }); // 新引用(哪怕函数体一字不差),加进去了,大小 2
funcSet.add(() => { count++; }); // 又一个新引用,大小 3
funcSet.forEach((func) => func()); // 三个函数各调一次
console.assert(count === 3, "三个函数各跑了一遍");这个剧本把「引用相等」演示到了不能再清楚:四个 add、三个引用、count 从 0 到 3。
若换用数组 + 循环查重,功能一样,但每加一个函数都要把数组重新扫一遍——
Set 的 has/add 是 O(1),回调列表一长,差距立刻可见。
13.2 元数据,displayName 与 name 的对照
React 的 displayName 例子,把「两个名字」的关系摆在一起:29
function HelloWorld() {
return <p>Hello, React world!</p>
}
HelloWorld.displayName = "HelloWorld"; // 手工挂的属性,压缩后仍在HelloWorld.name:声明时的名字,调用栈里显示的就是它; 但代码压缩(minification)会把名字改短改没,线上报错只剩at a (bundle.js:1:12345);HelloWorld.displayName:手工挂的属性,压缩工具不碰它, React DevTools 调试时优先显示它——名字没了,标签还在。
13.3 memoize,isPrime 的完整成本账
memoize 的「为什么值」要配一笔成本账才完整。isPrime(value) 的试除循环,
最坏情况(值是素数)要跑 value−2 次取模;判 5 是 3 次,判 997 是 995 次,
判一百万附近的素数约一百万次。若业务里同一个值会被查十次:30
不缓存:10 × ~1,000,000 次取模 ≈ 一千万次
缓存: 1 × 1,000,000(第一次)+ 9 × 1 次 Map 查找 ≈ 一百万次
─────────────────────────────────────────────
十倍差距,且缓存版本随查询次数线性「打折」(具体数值是为演示编的,量级关系是真的:缓存的查找是常数时间,计算是线性时间。) 书里点名的适用面:动画的逐帧计算、不常变数据的搜索、任何耗时的数学—— 判断标准一句话:同一个函数,会不会以相同输入被反复调用?
14. 深读四:参数与实参,四套规则的完整推导
14.1 「个数不齐」的全部四种组合
书里给了「多传」「少传」两条,把组合摆全(设函数是 function f(a, b)):
| 调用 | a | b | 多余的 | 结果 |
|---|---|---|---|---|
f(1, 2) | 1 | 2 | 无 | 正常 |
f(1) | 1 | undefined | — | b 缺省 |
f(1, 2, 3) | 1 | 2 | 3 被丢弃 | 无提示 |
f() | undefined | undefined | — | 全缺省 |
第三条是 JavaScript silent failure(静默失败)家族的一员: 多传的实参不报错、不警告,就那么消失。这也是「实参个数检查」只能交给 TypeScript(第 6 节)或 lint 工具的原因。31
14.2 rest 与「最后一个」的硬规则
rest 参数只许殿后,原因可以从语法上推出来:...rest 的含义是「剩下的都给我」——
如果后面还能跟别的参数,「剩下的」就无法定义了。解析器因此直接拒绝:
SyntaxError: parameter after rest parameter。这不是风格约定,是语法铁律。32
14.3 默认值的触发条件,精确到「显式 undefined」
书里的规则值得精确引用:默认值在「实参是 undefined」时生效——
所以下面三种调用对 function performAction(ninja, action = "skulking") 等价:33
performAction("Fuma"); // 没传 → undefined → 默认值
performAction("Fuma", undefined); // 显式传 undefined → 同样触发默认
performAction("Fuma", null); // null 不是 undefined → 不触发!action 是 null第三条是暗坑:null 常常被人当「没传」用,但默认值只认 undefined。
(TypeScript 里 action: string | null = "skulking" 的联合类型,正是为了
「null 有业务含义、undefined 才触发默认」这种场景准备的——见第 6 节。)
默认值表达式在每次调用时从左到右求值,还能引用前面的参数
(message = ninja + " " + action)。原书给的使用纪律:作为「避免 null」的手段、
作为配置函数行为的小开关——适度使用优雅,链条一长可读性就崩。34
15. 深读五:TypeScript 函数类型的完整演练
第 6 节是四段教程,这里把它们并成一条完整的故事线:同一个 compare 函数,
从「JavaScript 里怎么写」到「TypeScript 重载怎么写」,每一步解决上一步的哪个痛。
15.1 起点:JavaScript 的 compare 长什么样
function compare(a, b) {
if (typeof a === "string") {
if (typeof b === "number") throw new Error("Cannot compare a string to a number");
return a.localeCompare(b);
}
if (typeof b === "string") throw new Error("Cannot compare a number to a string");
if (a === b) return 0;
return a < b ? -1 : 1;
}功能对,但运行时靠 throw 守规矩:每次调用都要先跑两遍 typeof、准备好两条错误路径。 类型本该在编译期说清楚的事,被拖进了运行时的每一条调用。35
15.2 第一版 TypeScript:联合类型放行太多
function compare(a: string | number, b: string | number) { /* 同上 */ }
compare("a", "b"); // ✓
compare(1, 2); // ✓
compare("a", 3); // ✗ 编译也放行!运行时才会 throwstring | number 组合出四种配对,类型系统照单全收。要的精确度是
「两个参数同类型」——单个签名表达不了,于是上重载。
15.3 重载版:签名挡非法,实现管运行时
function compare(a: string, b: string): number; // 签名①:只管「两个字符串」
function compare(a: number, b: number): number; // 签名②:只管「两个数字」
function compare(a: string | number, b: string | number) { // 实现:宽类型,装下两种
if (typeof a === "string") {
return a.localeCompare(b as string); // as:签名保证过的事,告诉检查器
}
if (a === b) return 0;
return a < (b as number) ? -1 : 1;
}
compare("c", 3); // 编译期拒绝:不匹配任何签名 ✓
compare(3, "c"); // 同样拒绝 ✓两个设计点(书里都点了):其一,检查器只拿签名比对调用——
实现签名的宽类型不会泄出去;其二,实现里的 as 不是糊弄——
「能走到这行,说明调用已匹配某个签名,b 的类型必然和 a 一致」,
这是把签名的保证传递给检查器。36
(书还注明:截至 TypeScript 5.5,检查器尚不能从签名反推实现里的类型,所以 as 仍必要。)
箭头函数想重载,没有前置签名语法,走「函数类型起名」的同一条路:
type CompareFunction = {
(a: string, b: string): number;
(a: number, b: number): number;
};
const compare: CompareFunction = (a: string | number, b: string | number) => { /* 同实现 */ };类型写成「可调用的对象类型」,挂上多个调用签名——效果与函数重载完全一致。37
15.4 TSDoc 完整样板,与 .d.ts 的打包链
书里的 TSDoc 样板,值得作为模板记住:
/**
* 输出一个描述忍者行动的字符串。
* @param ninja — 忍者的名字
* @param action — 忍者在做的动作;null 表示什么都没做(默认 "skulking")
* @returns 形如 "Yagyu sneaking" 的字符串
*/
function performAction(ninja: string, action: string | null = "skulking") { ... }三个要点:自由文本段描述整体、@param 逐参数、@returns 标返回值;
@ 标记的顺序和数量随你,但这两个标记是最基本的组合。
链条的末端最妙:TypeScript 编译器生成 .d.ts(类型声明文件)时,会把 TSDoc 注释一并带进去——
于是你的包的消费者,在编辑器里悬停函数名,看到的不仅有类型,还有你写的说明。
「为你的包 API 写好 TSDoc,用户会感谢你」,这不是客套,是类型分发机制的一部分。38
15.5 返回类型声明,numberSign 的完整教学
numberSign 例子值得再走一遍,因为它是「声明返回类型能抓 bug」的最佳标本:39
// 没声明返回类型:检查器推断为 "positive" | "negative" | undefined——漏了零,但不报
const numberSign = (num: number) => {
if (num > 0) return "positive";
else if (num < 0) return "negative";
};
// 声明了返回类型:漏掉的分支立刻现形
const numberSign = (num: number): "positive" | "negative" => { ... };
// error TS2366: Function lacks ending return statement and return type does not include 'undefined'.报错信息本身就是最好的老师:「函数可能执行到末尾、而返回类型不含 undefined」——
它逼你把 "zero" 加进类型、把 return "zero" 加进实现。
「声明的类型越多,检查器帮得越多」,这条规律在此兑现得最直观。40
17. 深读六:函数定义的语法全貌,把书里的图补成文
17.1 函数声明的五段式(图 5.1 的文字版)
原书的图 5.1 把函数声明拆成五段,文字版照录,日后看到任何函数先对号入座:41
function 开场白:声明必须以这个关键字开头(表达式里它也是开头,身份由位置定)
samurai 名字:声明必填——只能靠名字调,没有名字就没有存在意义
(ninja) 参数表:逗号分隔的占位符,可空,括号必填
{ return …; } 函数体:语句列表,可空,花括号必填书里的 Listing 5.1 给了嵌套声明的样本——ninja 里套着 hiddenNinja,
C 家族语言里这是异端,在 JavaScript 里是正常的函数嵌套:42
function samurai() { return "samurai here"; } // 全局代码里的声明
function ninja() { // 也是全局代码
function hiddenNinja() { return "ninja here"; } // 函数体里的声明
return hiddenNinja();
}嵌套的语义预告(书里注了一句「save them for chapter 7」):内层函数对外层变量的 可见性,就是闭包;而内层声明会遮蔽外层同名声明,那是作用域规则的事。
17.2 函数表达式的六种栖身之处(Listing 5.2 的逐条注释)
书里的 Listing 5.2 一行展示了表达式的全部位置,逐条给注释:43
function myFunctionDeclaration() { // ① 独立语句 → 声明
function innerFunction() {} // ② 函数体里的独立语句 → 还是声明
}
const myFunc = function () {}; // ③ 赋值右边 → 表达式
myFunc(function () { // ④ 调用参数 → 表达式
return function () {}; // ⑤ 返回值 → 表达式(套娃)
});
(function namedFunctionExpression() {})(); // ⑥ 命名表达式,立即调用
+function () {}(); // ⑦–⑩ 四个一元运算符变体
-function () {}(); // +、-、!、~:表达式模式开关
!function () {}();
~function () {}();②值得停一秒:函数体里的 function innerFunction(){} 仍是声明——
「独立成句」的判定在函数体内同样生效。⑥的命名表达式有个细节:
名字 namedFunctionExpression 只在函数体内部可见(调试栈里能见到),
外部仍靠调用括号激活。
17.3 立即函数的两条失败路径,亲手复现
第 2 节讲了括号之谜,这里把两条失败路径写成可以亲手跑的实验:44
function () {}(3);
// SyntaxError: function 开头的语句必须是有名字的声明 → 解析失败
function f() {}(3);
// 不报错!但解析成【两条语句】:声明 f,和一个孤零零的 3 → 静默 no-op
// f 从没被调用过
(function () {})(3); // ✓ 括号逼解析器进表达式模式,(3) 被当成调用
(function () {}(3)); // ✓ 等价变体:调用也包进括号里第二条最阴险:它不报错、什么都不做,让你的「初始化代码」悄悄失效。 「看到 function 开头就多想想」是读老代码的自我保护。
18. 深读七:回调的执行全景,从日志到时间线
18.1 useless(getText) 的完整时间线
书里的 Listing 5.4 带完整日志输出,把执行过程画成时间线:45
入口:模块开始执行
│
├─ 定义 useless、getText(函数定义不产生动作) 日志:Before defining functions
├─ console.assert(useless(getText) === text, …) 所在行 日志:Before making all the calls
│ │
│ ├─ 进入 useless ─────────────────────────────► 日志:In useless function
│ │ └─ ninjaCallback() 即 getText() ──────► 日志:In getText function
│ │ 返回 text("Domo arigato!")
│ └─ useless 返回 text,断言通过(无输出)
└─ 模块结束 日志:After the calls have been made这个顺序本身就是教学:函数定义是静态的,动作全部发生在调用点; 「回调」没有特殊的执行机制——就是普通函数调用,只不过调用者是被传进去的那个函数。
18.2 回调内联化的取舍
书里把 getText 内联进调用点,展示了回调的另一种写法:46
console.assert(useless(function () { return text; }) === text, "…");取舍书里点了两头:代码更紧凑、函数定义紧贴使用处; 但也消灭了名字——如果这段逻辑在别处还要用,具名更值。 「就地定义」和「起个名字」之间的天平,由复用次数决定:一次用,内联;多次用,具名。
19. 深读八:参数机制的四个完整剧本
19.1 practice 函数的四种调用全表
设 function practice(ninja, weapon, technique),四种实参组合的结果:47
practice("Yoshi", "sword", "shadow sword") → 三个参数各得其所
practice("Yoshi", "sword") → technique = undefined
practice("Yoshi") → weapon、technique 都是 undefined
practice("Yoshi", "sword", "影", "katana") → "katana" 无人接,静默丢弃注意两个不对称:多传的丢弃无声,少传的补 undefined 也无声—— JavaScript 在实参个数上没有任何运行时怨言,这正是「签名是文档不是契约」的语言, 契约要交给 TypeScript(第 6 节)或 lint 工具去补。
19.2 rest 参数的「收拢」图解
multiMax(3, 1, 2, 3) 的收拢过程:
实参队列: 3 1 2 3
│ └────┬────┘
│ 收拢成数组
▼ ▼
参数: first ...remainingNumbers
= 3 = [1, 2, 3]first 拿第一个,...remainingNumbers 把剩下的全部收成一个真数组——
可以 sort、可以取下标、有 length。它是 ES2015 之前 arguments 对象的正式替代:
arguments 是「像数组的对象」(没有数组方法),rest 是真数组,还能和解构、
默认参数、TypeScript 注解自由组合。48
19.3 默认参数的「每调用求值」与引用前参
书里的 Listing 5.11:
function performAction(ninja, action = "skulking", message = ninja + " " + action) {
return message;
}带数走一遍 performAction("Yoshi"):action 缺 → "skulking";message 缺 →
求值 ninja + " " + action = "Yoshi skulking"。默认值表达式每次调用、从左到右
求值——所以如果默认值有副作用(比如 Date.now()),每次不传都会触发一次。
原书对「引用前参」这朵花的判词再引一次:「urge caution……doesn't enhance code
readability」——能用,但问问自己:省下的是字符,丢掉的是可读。49
20. 深读九:TypeScript 函数检查的三个补充剧本
20.1 运行时检查 vs 编译期检查的对照账
第 6 节开场给了 JavaScript 运行时检查的样板,把两版的「成本与收益」摆成账:50
JavaScript 版:doubleNumber
成本:每次调用都跑 typeof + 条件 + 拼错误消息;错误消息要自己写;
库没写的函数,你调之前没法知道它查不查
收益:运行时真能拦住坏输入(如果写了的话)
TypeScript 版:doubleNumber
成本:一个 : number 注解;编译器把注解剥掉(零运行时成本)
收益:编译期拦 doubleNumber("abc"),报错带行号列号;
编辑器悬停可见签名账的结论就是原书的判断:「类型注解没有运行时行为」的简单性,是 TypeScript 成功的 大原因——类型层做的事再多,跑起来的代码和手写 JavaScript 逐字节一致。
20.2 重载的「箭头函数版」完整对照
第 6 节的函数重载只给了 function 关键字版,箭头函数版书里的 Listing 5.17 照抄成模板:51
type CompareFunction = { // 可调用类型:一个对象类型,带多张调用签名
(a: string, b: string): number;
(a: number, b: number): number;
};
const compare: CompareFunction =
(a: string | number, b: string | number) => {
if (typeof a === "string") {
return a.localeCompare(b as string);
}
if (a === b) return 0;
return a < (b as number) ? -1 : 1;
};类型在左、实现在右:类型层可以任意精确(几张签名都行),实现层永远只有一个函数。 这与 C++ 的重载(每个签名一份实现)是两种世界观——TypeScript 的世界里, 类型是运行时代码的一张「宣传海报」,可以比本尊华丽。
20.3 TSDoc 的剩余 @ 标记与校验工具
书里说 @param 和 @returns 是「最 essential」的组合,另指了两个延伸:
tsdoc.org 上列着更丰富的 @ 标记家族;eslint-plugin-tsdoc 可以校验
TSDoc 注释的语法——写错了标记名,lint 阶段就报,不用等到生成文档才发现。
写公开 API 的团队,把这条 lint 配上,注释质量就有了下限。52
21. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 「函数式语言」宣言 | Chapter 5: First-class functions for the novice: definitions and arguments | text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:14(搜「functional language」) |
| 一等对象定义 | 同上 | :23(搜「first-class objects」) |
| 六种定义方式清单 | 同上 | :41(搜「bevy of ways」) |
| 声明与表达式的语法分水岭 | 同上 | :109(搜「its own JavaScript statement」) · :165(搜「part of another statement」) |
| 一处六种用法代码 | 同上 | :172(搜「Listing 5.2」) |
| IIFE 与括号之谜 | 同上 | :236(搜「IMMEDIATE FUNCTIONS」) · :253(搜「parser must be able to differentiate」) |
| 箭头函数语法两档 | 同上 | :294(搜「Arrow functions」) · :314(搜「param => expression」) |
| 函数=对象四对照 | 同上 | :397(搜「versatility of JavaScript functions」) |
| 函数式编程边栏 | 同上 | :471(搜「FUNCTIONAL PROGRAMMING IN JAVASCRIPT」) |
| useless 回调与输出 | 同上 | :488(搜「completely useless function」) · :503(搜「Listing 5.4」) |
| sort 比较器与默认排序坑 | 同上 | :587(搜「SORTING WITH A COMPARATOR」) · :616(搜「converts each value to a string」) |
| Set 存函数与引用相等 | 同上 | :636(搜「Storing functions in a collection」) · :670(搜「reference equality」) |
| 挂元数据与封装警告 | 同上 | :679(搜「Attaching metadata」) · :688(搜「doesn't mean we should」) |
| memoize 定义与实现 | 同上 | :717(搜「Memoizing functions」) · :756(搜「Listing 5.8」) |
| 单参数限制与 lodash/memoize-one | 同上 | :779(搜「only works for functions」) |
| 参数 vs 实参定义 | 同上 | :789(搜「parameter is a variable」) |
| 多传丢弃/少传 undefined | 同上 | :820(搜「Excess arguments」) · :833(搜「set to undefined」) |
| rest 参数与带数例子 | 同上 | :841(搜「Rest parameters」) · :851(搜「Listing 5.9」) |
| 默认参数与 undefined 触发 | 同上 | :889(搜「Default parameters」) · :943(搜「left out」) |
| 引用前参数的默认值 | 同上 | :969(搜「Referencing previous parameters」) · :979(搜「urge caution」) |
| TypeScript 参数/返回类型 | 同上 | :1013(搜「Defining parameter types」) · :1072(搜「Defining return types」) |
| numberSign 漏分支被抓住 | 同上 | :1109(搜「numberSign」) |
| 函数重载与 C++ 对比 | 同上 | :1145(搜「function overloading」) · :1207(搜「very different from function overloads in languages like C++」) |
| TSDoc 与 .d.ts | 同上 | :1241(搜「Documenting functions with TSDoc」) · :1279(搜「generates .d.ts files」) |
| 全章总结 | 同上 | :1288(搜「Summary」) |
Footnotes
-
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 10–21 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:14,搜「functional language」)。「the main difference between writing JavaScript code like the average Jill (or Joe) and writing it like a JavaScript ninja is understanding JavaScript as a functional language」。 ↩ -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 23–30 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:23,搜「first-class citizens」)。一等对象/一等公民的待遇清单。 ↩ -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 41–89 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:41,搜「bevy of ways」)。六种定义方式的原书清单(声明、表达式、箭头、方法简写、类字段、Function 构造器)。 ↩ -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 98–111 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:110,搜「its own JavaScript statement」)。声明必须独立成句的限制;提升行为本章只预告(第 45–47 段)。 ↩ -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 149–219 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:165,搜「assignment, or as an argument to another function」)。表达式总依附于别的语句;名字可选的理由(第 217–219 段)。 ↩ -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 172–196 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:174,搜「myFunctionDeclaration」)。Listing 5.2 一处六种用法的完整代码。 ↩ -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 236–251 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:251,搜「immediately invoked function expression」)。IIFE 命名与「任何求值为函数的表达式 + 调用括号」的原理。 ↩ -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 253–275 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:257,搜「purely syntactical」)。括号逼解析器进表达式模式的完整论证;function f(){}(3)被拆成两条语句的陷阱。 ↩ -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 294–312 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:306,搜「hypotenuse」)。hypotenuse 两版对照。 ↩ -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 314–334 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:361,搜「The syntax of an arrow function」)。箭头函数语法总览(Figure 5.3):简式与块式。 ↩ -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 389–395 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:390,搜「declared with the function keyword. There’s also an」)。this 行为差异预告,第 6 章兑现。 ↩ -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 65–89 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:79,搜「Function constructor」)。方法简写、类字段、Function 构造器的原书条目;「used with caution」的安全警告。 ↩ -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 88–89 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:88,搜「generators」)。生成器留待第 8 章的说明。 ↩ -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 397–431 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:399,搜「Assigned to variables」)。对象四项能力与函数逐项复刻的原书对照。 ↩ -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 471–481 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:474,搜「composing functions」)。函数式编程的边栏定义与原书的边界声明(「in this book we only give it a nod」)。 ↩ -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 483–496 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:489,搜「Whenever we set up a function to be」)。回调的术语来源:establishing a function that other code will later "call back"。 ↩ -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 503–555 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:540,搜「Before defining functions」)。Listing 5.4 的完整执行日志,顺序即教学。 ↩ -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 587–615 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:599,搜「callbacks jump in」)。比较器契约:正数换序、负数保序、零等价。 ↩ -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 616–621 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:617,搜「converts each value to a string」)。默认排序转字符串按字典序、10 排在 2 前的坑。 ↩ -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 636–648 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:637,搜「collections of callbacks」)。回调集合去重的场景与原书对数组查重的嫌弃。 ↩ -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 670–677 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:671,搜「reference equality」)。「uniqueness」= 引用相等的精确含义;两个同体函数是两个对象。 ↩ ↩2 -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 698–714 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:710,搜「displayName with the same value? Mainly because function」)。displayName 用例与「函数名被压缩毁掉、手工属性保留」的对比。 ↩ ↩2 -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 688–696 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:688,搜「doesn't mean we should」)。封装原则与「更优雅的做法是模块作用域里的独立变量」。 ↩ ↩2 -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 717–724 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:718,搜「no, that's not a typo」)。memoization 拼写说明与定义:记住已算结果,同样的参数不再重算。 ↩ ↩2 -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 756–784 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:758,搜「function memoize」)。Listing 5.8 的 memoize 实现;「encapsulation: No other code can access the cached results」见第 775–777 段。 ↩ ↩2 -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 779–784 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:779,搜「single argument」)。单参数限制与 lodash/memoize-one 的出处。 ↩ ↩2 -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 789–798 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:793,搜「A parameter is a variable」)。参数与实参的正式定义。 ↩ ↩2 -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 815–836 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:820,搜「no error is raised」)。个数不齐不报错的总规则与两个方向的具体行为。 ↩ ↩2 -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 841–884 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:853,搜「function multiMax(first, ...remainingNumbers) {」)。Listing 5.9 与 multiMax(3,1,2,3)=9 的逐步说明(第 872–879 段)。 ↩ ↩2 -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 889–962 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:919,搜「action = "skulking"」)。默认参数语法与触发条件;TypeScript 节确认 undefined 显式传入同样触发默认(第 1050–1055 段)。 ↩ ↩2 -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 964–982 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:979,搜「urge caution」)。表达式默认值的合法性与可读性警告。 ↩ ↩2 -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 984–1000 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:989,搜「Checking argument types」)。JavaScript 运行时查类型样板与「最常见的错误源」判断。 ↩ ↩2 -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 1025–1027 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:1025,搜「When the TypeScript compiler converts this code to」)。注解被剥除、无运行时检查、无成本的三连说明。 ↩ ↩2 -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 1028–1066 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:1040,搜「type from the default」)。默认值反推类型;string | null 联合的写法。 ↩ ↩2 -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 1072–1098 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:1096,搜「(str: string) => string」)。返回值推断与编辑器里可见的完整签名。 ↩ ↩2 -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 1099–1139 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:1121,搜「TS2366」)。numberSign 漏掉零被抓的完整例子与原书总结(「the more types you declare, the more the type checker can help you」)。 ↩ ↩2 -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 1145–1184 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:1181,搜「preface it with additional type signatures」)。重载的定义方式与检查器「只认签名」的语义。 ↩ ↩2 -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 1216–1239 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:1224,搜「CompareFunction」)。箭头函数经函数类型实现重载的样板。 ↩ ↩2 -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 1207–1214 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:1208,搜「very different from function overloads in languages like C++」)。TypeScript 重载与 C++ 的本质差异;「never adds code you didn't write」的哲学声明。 ↩ ↩2 -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 1241–1286 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:1250,搜「tsdoc.org」)。TSDoc 的定位;「compiler generates .d.ts files, it includes TSDoc comments」的打包机制(第 1279–1283 段)。 ↩ ↩2 -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 253–271 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:261,搜「parser error」)。去掉括号的两条失败路径(语法错/静默 no-op)。 ↩ ↩2 -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 775–777 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:776,搜「encapsulation」)。缓存不可外部访问的设计理由。 ↩ ↩2 -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 1046–1055 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:1051,搜「undefined」)。三种调用(省略/显式 undefined/显式值)的类型检查结论。 ↩ ↩2 -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 253–275 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:261,搜「parser error」)。去掉括号的两条失败路径与(function(){})(3)的两种包法。 ↩ -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 503–555 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:540,搜「Before defining functions」)。Listing 5.4 的完整日志输出。 ↩ -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 556–575 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:568,搜「Defines a callback function directly as an argument」)。回调内联化写法及其紧凑性动机。 ↩ -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 815–838 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:829,搜「shadow sword」)。practice 调用的实参分配示例。 ↩ -
出处:「Chapter 6: Functions for the journeyman: understanding function invocation」第 31–41 段(
text/09-ch06-chapter-6-functions-for-the-journeyman-understan.txt:31,搜「arguments」)。arguments 对象退场的边栏:rest 参数是它的正式替代。 ↩ -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 964–982 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:976,搜「performAction("Yoshi")」)。Listing 5.11 的调用结果;「urge caution」见第 979–981 段。 ↩ -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 984–1027 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:1025,搜「simply strips out」)。运行时检查样板与「零运行时成本」论断。 ↩ -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 1216–1239 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:1222,搜「overload signatures」)。Listing 5.17 的可调用类型重载。 ↩ -
出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 1275–1286 段(
text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:1285,搜「eslint-plugin-tsdoc」)。TSDoc 校验工具与「write good TSDoc」的收尾建议。 ↩