跳转至

一等函数(上)— 定义、回调与参数的全部花样

这一章讲三件事: 定义一个函数居然有六种写法,而且行为不完全一样; 「函数是值」这件事能玩出什么花样(存进集合、挂上数据、包上缓存); 以及参数与实参这对「听起来是一个词」的概念,加上两个 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,存入

这个壳的三个设计点都是「函数是一等对象」的果实:

  1. 包装而不改原函数:memoizedIsPrime 是新函数,isPrime 原样保留——想不用缓存,用原版;
  2. 缓存封装在闭包里:cache 是 memoize 体内的局部变量,只有返回的壳能摸到—— 这是第 5 章第一次、全书无数次用上闭包的地方;
  3. 包装器可复用: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. 可带走的(合上这一章,你应该有这十条)

  1. 一等对象四条:能字面量写出、能赋给变量/属性、能当参数、能当返回值——函数全占,外加「可被调用」。
  2. 声明 vs 表达式:声明必须独立成句、名字必填、可被提升;表达式是别的语句的一部分、可匿名。 见到以 function 开头的语句,解析器铁定按声明读。
  3. IIFE = 「造函数立刻调」;外层的括号(或 + ! 等一元运算符)是逼解析器按表达式读的语法手段。
  4. 箭头函数两档语法:简式直接返回表达式,块式要自己写 return;单参数可省括号。
  5. 回调 = 把函数交给别人回头调;排序比较器是它的标本:算法归系统,裁决归你。
  6. 函数进 Set 按引用去重;函数体相同 ≠ 同一函数。挂属性可以,但想想封装——元数据是少数正当用途。
  7. memoize = 闭包包一个 Map:真算一次、缓存结果、壳可复用、原函数不动。
  8. 参数是定义里的占位符,实参是调用时的真身;多传丢弃、少传补 undefined,永不报错。
  9. rest 收拢(...rest 在定义处)、默认兜底(action = "skulking");rest 必须是最后一个参数。
  10. 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);    // ✗ 编译也放行!运行时才会 throw

string | 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」)

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

  2. 出处:「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」)。一等对象/一等公民的待遇清单。 ↩

  3. 出处:「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 构造器)。 ↩

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

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

  6. 出处:「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 一处六种用法的完整代码。 ↩

  7. 出处:「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 命名与「任何求值为函数的表达式 + 调用括号」的原理。 ↩

  8. 出处:「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) 被拆成两条语句的陷阱。 ↩

  9. 出处:「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 两版对照。 ↩

  10. 出处:「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):简式与块式。 ↩

  11. 出处:「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 章兑现。 ↩

  12. 出处:「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」的安全警告。 ↩

  13. 出处:「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 章的说明。 ↩

  14. 出处:「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」)。对象四项能力与函数逐项复刻的原书对照。 ↩

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

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

  17. 出处:「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 的完整执行日志,顺序即教学。 ↩

  18. 出处:「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」)。比较器契约:正数换序、负数保序、零等价。 ↩

  19. 出处:「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 前的坑。 ↩

  20. 出处:「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」)。回调集合去重的场景与原书对数组查重的嫌弃。 ↩

  21. 出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 41–92 段(text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:41,搜「bevy of ways」)。六种定义方式清单;类字段「some unique features」见第 74–77 段。 ↩↩

  22. 出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 98–112 段(text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:105,搜「mandatory function keyword」)。声明五段结构与独立成句限制。 ↩↩

  23. 出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 217–234 段(text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:222,搜「alternative ways to invoke them」)。表达式可匿名的两条调用通道;「eliminate the need to pollute the module namespace」见第 570–574 段。 ↩↩

  24. 出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 277–289 段(text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:286,搜「unary operators」)。四个一元运算符变体与「only the calls to our IIFEs matter」。 ↩↩

  25. 出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 314–392 段(text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:316,搜「param => expression」)。箭头函数语法与括号规则;「arrow function operator」正式名见第 312–313 段。 ↩↩

  26. 出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 488–584 段(text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:495,搜「button click」)。回调的三种雇主场景(事件、服务器、UI 动画)原书枚举。 ↩↩

  27. 出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 587–621 段(text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:617,搜「converts each value to a string」)。默认排序的字典序行为与 localeCompare 建议。 ↩↩

  28. 出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 650–677 段(text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:676,搜「Adds another function to the set」)。同体函数两个引用、都进 Set 的例子。 ↩↩

  29. 出处:「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:711,搜「displayName are preserved」)。displayName 与压缩行为的对照。 ↩↩

  30. 出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 725–754 段(text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:727,搜「deriving the MD5 hash」)。memoize 的适用面列举(动画、数据搜索、耗时数学)。 ↩↩

  31. 出处:「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:820,搜「no error is raised」)。个数不齐的处理规则;「excess arguments aren't assigned」见第 823–826 段。 ↩↩

  32. 出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 880–884 段(text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:882,搜「Trying to put the ellipsis」)。rest 必须殿后的语法铁律。 ↩↩

  33. 出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 939–962 段(text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:946,搜「left out」);与第 1050–1055 段。默认值触发条件与 TypeScript 下 undefined 的行为确认。 ↩↩

  34. 出处:「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:966,搜「evaluated on each function call」)。默认值求值时机与「moderate use」的使用纪律。 ↩↩

  35. 出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 1145–1179 段(text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:1154,搜「single type signature」)。compare 单签名版的运行时类型检查问题。 ↩↩

  36. 出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 1180–1206 段(text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:1203,搜「only allow calls that match」)。重载语义:检查器只认签名;as 传递签名保证。 ↩↩

  37. 出处:「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」)。箭头函数经函数类型实现重载的完整样板。 ↩↩

  38. 出处:「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:1279,搜「generates .d.ts files」)。TSDoc 标记体系与「编译器把注释打进 .d.ts」的分发机制。 ↩↩

  39. 出处:「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:1109,搜「numberSign」)。numberSign 两版对照与 TS2366 报错原文。 ↩↩

  40. 出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 1136–1139 段(text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:1139,搜「the more types you declare」)。「声明越多、帮得越多」规律的原话。 ↩↩

  41. 出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 98–112 段(text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:105,搜「mandatory function keyword」)。图 5.1 的五段式与「声明必须独立成句」。 ↩↩

  42. 出处:「Chapter 5: First-class functions for the novice: definitions and arguments」第 114–131 段(text/08-ch05-chapter-5-first-class-functions-for-the-novice-d.txt:116,搜「function samurai」)。嵌套函数声明样本与「perfectly normal」的定性。 ↩↩

  43. 出处:「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:172,搜「Listing 5.2」)。Listing 5.2 的逐条注释(#A–#G)。 ↩↩

  44. 出处:「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) 的两种包法。 ↩

  45. 出处:「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 的完整日志输出。 ↩

  46. 出处:「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」)。回调内联化写法及其紧凑性动机。 ↩

  47. 出处:「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 调用的实参分配示例。 ↩

  48. 出处:「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 参数是它的正式替代。 ↩

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

  50. 出处:「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」)。运行时检查样板与「零运行时成本」论断。 ↩

  51. 出处:「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 的可调用类型重载。 ↩

  52. 出处:「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」的收尾建议。 ↩