# 箭头函数

Source: https://codewiki.com/zh/javascript/arrow-functions/

> - **what**: 箭头函数（arrow function）是使用 `=>` 写出的函数表达式。它可以采用表达式函数体并隐式返回结果，而且不创建自己的 `this`、`arguments`、`super` 或 `new.target` 绑定。
> - **trap**: 箭头函数不是普通函数的全面简写。把对象方法、构造函数或依赖动态接收者的回调机械改成箭头函数，会改变程序语义。
> - **fix**: 需要继承外层 `this` 的回调使用箭头函数；需要由调用方式决定 `this`、需要构造能力或需要自身 `arguments` 时，使用普通函数或方法。

## 是什么，为什么存在

箭头函数是一种函数表达式，而不是函数声明。它用 `=>` 连接参数与函数体，可以把短小的转换写成 `value => value * 2`，也可以像普通函数一样使用花括号函数体。语法更短只是表面差异，词法上下文才是选择它的关键。

普通函数调用时，调用表达式会参与决定 `this`。箭头函数不创建自己的 `this`，而是沿定义位置的词法环境（lexical environment）查找它。这个规则适合需要保留外层接收者的回调函数（callback），例如方法内部安排的异步工作。

箭头函数也没有自己的 `arguments`、`super` 和 `new.target` 绑定。它不能通过 `new` 调用，也不能写成生成器。把它看成「更短的普通函数」会漏掉这些限制。

最稳妥的选择标准是调用契约。只需要参数并返回结果，或者回调明确需要外层 `this` 时，箭头函数通常很合适；需要调用方提供接收者、作为构造器使用，或需要普通方法的原型共享行为时，应选普通函数或方法语法。

你会在数组转换、Promise 链、事件订阅和函数工厂中经常看到箭头函数。出现频率不等于默认正确，尤其是对象字面量方法与类字段：两处都能写 `=>`，但它们的 `this`、属性位置和函数标识不同。

## 工作原理

箭头函数每次求值都会创建一个新的可调用对象。参数列表可以为空、包含多个参数，或包含默认值、解构与剩余参数（rest parameter）。只有单个简单参数可以省略圆括号。

函数体分为表达式函数体和块函数体。表达式函数体把表达式值作为返回值；块函数体遵循普通语句规则，必须显式执行 `return` 才会返回该值。紧跟 `=>` 的 `{` 会开始块，而不是自动表示对象字面量。

| 形式 | 含义 | 关键限制 |
| --- | --- | --- |
| `value => value.id` | 单参数、表达式函数体 | 直接返回表达式值 |
| `(left, right) => left + right` | 多参数 | 参数必须放在圆括号中 |
| `({ id }) => id` | 解构参数 | 参数必须放在圆括号中 |
| `(...values) => values.length` | 剩余参数 | `values` 是真正的数组 |
| `value => ({ value })` | 返回对象字面量 | 对象必须用圆括号包裹 |
| `value => { return value; }` | 块函数体 | 省略 `return` 会得到 `undefined` |

词法 `this`（lexical this）表示箭头调用不会执行普通函数的接收者绑定步骤。`call()`、`apply()` 和 `bind()` 仍能调用箭头函数或预置实参，但它们提供的 `thisArg` 不会替换箭头函数从外层解析到的 `this`。

「继承外层 `this`」不表示箭头函数复制了某个对象。它表示名称解析继续使用外层 `this` 绑定。箭头函数在普通方法调用期间创建时会看到该方法的接收者；在 ECMAScript 模块顶层创建时，外层 `this` 是 `undefined`。

`arguments` 的规则相同。箭头函数体中的 `arguments` 若能解析到值，那个值来自外层普通函数；若外层环境没有该绑定，访问就会失败。新代码需要收集本次箭头调用的实参时，应显式写出剩余参数。

箭头函数可调用但不可构造，因为它没有 `[[Construct]]` 内部能力。`new Arrow()` 会抛出 `TypeError`，箭头函数通常也没有自己的 `prototype` 属性。后者是可观察结果，不是判断所有函数能否构造的通用测试。

函数标识仍然重要。每次求值 `() => work()` 都会产生不同函数，即使源码文本完全相同。注册和注销回调的 API 通常要求传回同一个函数对象，因此应把回调保存到稳定的位置。

## 示例

下面四个示例依次展示表达式函数体、词法 `this`、剩余参数和类字段回调。所有输出都由本地 Node v24.14.0 执行对应文件得到。

### 从表达式返回数据

数组回调只依赖显式参数，不需要动态接收者，因此很适合箭头函数。`toSummary` 返回对象字面量，所以对象外层必须加圆括号。

<!-- quick -->

```javascript
// file: order_summary.js
const orders = [
  { id: 'A-17', total: 125, paid: true },
  { id: 'B-04', total: 60, paid: false },
  { id: 'C-03', total: 42.5, paid: true },
];

const paidOrders = orders.filter(({ paid }) => paid);
const toSummary = ({ id, total }) => ({
  id,
  label: `${id}: $${total.toFixed(2)}`,
});
const summaries = paidOrders.map(toSummary);
const paidTotal = paidOrders.reduce((sum, order) => sum + order.total, 0);

console.log(JSON.stringify(summaries));
console.log(`paid total: $${paidTotal.toFixed(2)}`);
```

```text
[{"id":"A-17","label":"A-17: $125.00"},{"id":"C-03","label":"C-03: $42.50"}]
paid total: $167.50
```


<!-- /quick -->

`filter()` 的解构参数需要圆括号，`reduce()` 则使用两个简单参数。表达式函数体适合这种单一结果；一旦转换需要校验、多个分支或中间变量，块函数体配合显式 `return` 通常更清楚。

这个示例没有依赖隐式 `this`。因此把回调传给数组方法时，调用方怎样设置接收者都不会影响结果；输入与返回值已经完整表达了依赖关系。

### 保留创建位置的 `this`

`makeFormatter` 是普通函数，因此 `call(west, 'EUR')` 能为这次工厂调用设置 `this`。工厂返回的箭头函数继续使用该绑定，后续对箭头调用 `call(east, 8)` 只传入金额，不能换掉接收者。

```javascript
// file: lexical_this.js
function makeFormatter(currency) {
  return (amount) =>
    `${this.account}: ${currency} ${amount.toFixed(2)}`;
}

function formatWithoutArrow(amount) {
  return `${this.account}: ${amount.toFixed(2)}`;
}

const west = { account: 'west' };
const east = { account: 'east' };
const formatEuro = makeFormatter.call(west, 'EUR');

console.log(formatEuro(12.5));
console.log(formatEuro.call(east, 8));
console.log(formatWithoutArrow.call(east, 8));
```

```text
west: EUR 12.50
west: EUR 8.00
east: 8.00
```

最后一次调用使用普通函数，所以 `east` 成为它的接收者。这个对比说明 `call()` 没有失效；它仍按普通方式调用函数，只是箭头函数没有可供替换的自身 `this` 绑定。

如果工厂本身被当作普通函数直接调用，外层 `this` 取决于那个调用所在的脚本类型与严格模式。不要依赖模糊的顶层接收者，应该通过方法调用、明确的 `call()`，或更直接的普通参数建立所有权。

### 区分外层 `arguments` 与本次实参

`readFactoryArguments` 是箭头函数，所以它读取 `makeInspector` 的 `arguments`。返回的箭头函数用 `...values` 收集自己的实参，两组数据不会混在一起。

```javascript
// file: rest_parameters.js
function makeInspector(label) {
  const readFactoryArguments = () => Array.from(arguments);

  return (...values) => ({
    label,
    factoryArguments: readFactoryArguments(),
    values,
    valuesAreArray: Array.isArray(values),
  });
}

const inspectBatch = makeInspector('batch', 99);
console.log(JSON.stringify(inspectBatch('A', 'B')));
```

```text
{"label":"batch","factoryArguments":["batch",99],"values":["A","B"],"valuesAreArray":true}
```

剩余参数直接得到数组，可以使用数组方法，也不会意外读取外层调用。只有维护依赖旧式 `arguments` 行为的代码时，才需要刻意沿词法环境访问它。

参数命名也让接口更容易审查。看到 `(...values)` 就能确定函数接受可变数量的值；裸写 `arguments` 则必须先找出它究竟属于哪一层普通函数。

### 用类字段保存回调

类字段初始化期间，`this` 指向正在构造的实例。字段中的箭头函数因此可在脱离属性访问后继续使用实例，但每个实例都会得到自己的函数对象。

```javascript
// file: class_field_callback.js
class ClickCounter {
  count = 0;

  constructor(name) {
    this.name = name;
  }

  handleClick = () => {
    this.count += 1;
    return `${this.name}:${this.count}`;
  };
}

const first = new ClickCounter('first');
const second = new ClickCounter('second');
const detached = first.handleClick;

console.log(detached());
console.log(detached());
console.log(Object.hasOwn(first, 'handleClick'));
console.log(first.handleClick === second.handleClick);
```

```text
first:1
first:2
true
false
```

`handleClick` 是实例自有属性，不在 `ClickCounter.prototype` 上。稳定的字段值适合直接注册和注销；若在两处分别写新的包装箭头，得到的标识仍然不同。

普通原型方法由实例共享，但脱离方法调用后不会自动保留实例。两种写法都有效，选择取决于是否需要稳定的已绑定回调、原型覆盖与测试替换方式，而不是统一的风格偏好。

## 陷阱

### 把对象方法机械改成箭头函数

> **陷阱:** 对象字面量中的 `read: () => this.value` 不会因为通过 `object.read()` 调用就取得 `object`。箭头忽略该调用形式提供的接收者，继续从对象字面量外层解析 `this`。

**修复方法：** 需要对象接收者时使用 `read() { return this.value; }` 这种方法语法。若函数本来就不依赖对象，则使用箭头函数并通过参数传入所需数据，避免留下误导性的 `this`。

### 在块函数体中漏掉 `return`

> **陷阱:** 把 `item => ({ id: item.id })` 扩展成带日志的 `item => { log(item); { id: item.id }; }` 后，函数会返回 `undefined`。花括号切换到块函数体，内部对象还可能被解析成标签语句。

**修复方法：** 块函数体显式写 `return { id: item.id };`。若只返回对象表达式，则保持 `item => ({ id: item.id })`，并为返回值添加断言而不只验证副作用。

### 用新箭头函数注销回调

> **陷阱:** `on('data', value => consume(value))` 与稍后的 `off('data', value => consume(value))` 创建了两个不同函数。源码看起来相同，注销 API 仍找不到原先注册的标识。

**修复方法：** 创建一次回调并保存引用，例如放在实例字段或拥有该订阅的局部变量中。生命周期测试应执行「注册、触发、注销、再次触发」，证明最后一步不再调用处理器。

### 重构后仍使用普通函数能力

> **陷阱:** 自动重构常把构造函数或读取自身 `arguments` 的函数改成箭头，却保留 `new` 调用和原有函数体。结果可能在调用点抛出 `TypeError`，也可能静默读取某个外层函数的 `arguments`。

**修复方法：** 重构前搜索 `new`、`prototype`、`this`、`arguments`、`super` 与 `new.target` 的所有用法。需要本次调用实参时改成剩余参数；需要构造或动态接收者时保留普通函数、方法或类。

### 把类字段箭头当作原型方法

> **陷阱:** 箭头函数字段是每个实例上的自有属性。依赖 `Class.prototype.method` 的覆盖、监视或补丁可能拦截不到它，创建新实例也会得到新的函数标识。

**修复方法：** 需要共享实现和原型多态时使用普通方法，并在订阅边界保存一次包装或绑定函数。确实需要脱离调用仍保留实例时，可以使用箭头函数字段，但应测试自有属性、覆盖行为和注销路径。

<!-- deep -->

## 语法边界与解析

### 参数列表

没有参数时必须写 `()`，多个参数也必须放入圆括号。默认参数、解构参数和剩余参数都不是简单单参数，因此即使视觉上只有一项，也不能省略圆括号。统一保留单参数圆括号属于格式选择，不改变语义。

参数与 `=>` 之间不能出现换行。这是语法限制，不是自动分号插入产生的普通语句边界。需要换行时，可以在圆括号内部排列参数，或把换行放在箭头之后。

箭头函数没有声明形式。`const parse = value => value.trim()` 的可用时间由 `const` 声明决定，初始化前处于暂时性死区；不能笼统地把这一点说成「箭头函数不会提升」。如果把箭头赋给 `var` 或对象属性，相关绑定规则又不同。

### 表达式函数体与块函数体

表达式函数体只能包含一个表达式，该表达式的结果就是返回值。条件表达式、函数调用、数组字面量和用圆括号包裹的对象字面量都可以放在这里。逻辑复杂度增加时，不应为了保留单行形式而堆叠多层条件表达式。

块函数体可以包含任意合法语句，但到达末尾时与普通函数一样返回 `undefined`。`async` 箭头函数的块函数体漏掉 `return` 时，会得到兑现值为 `undefined` 的 Promise；异步外观不会补回结果。

对象字面量的歧义来自语法解析。`value => { key: value }` 中，`{}` 被当作块，`key:` 被当作标签，因此整个函数通常返回 `undefined`。写成 `value => ({ key: value })` 后，圆括号迫使解析器把花括号解释为表达式。

箭头不是普通二元运算符，和其他表达式组合时有专门的语法限制。赋值、空值合并或逻辑表达式若把一个箭头函数当作右侧值，显式圆括号常能同时消除解析歧义与阅读歧义。

### 名称与递归

箭头函数语法本身不提供像具名函数表达式那样的内部名称。赋给变量或属性时，引擎通常会推断可观察的 `name`，但函数体自我调用仍依赖外部绑定。如果该绑定后来被重新赋值，递归路径也随之改变。

需要稳定自引用时，具名函数表达式通常更直接。它的内部名称只在函数体内可见，不依赖外层变量继续指向原函数。名称推断主要服务调试与堆栈显示，不应被当作新的词法绑定。

## 词法上下文

### `this` 的来源

普通函数会根据调用形式绑定 `this`，例如属性调用提供基值，`call()` 明确提供 `thisArg`，构造调用提供新实例。箭头函数跳过这一步，继续在外层环境中解析 `this`。所以真正要问的是「箭头在哪里创建」，而不是「箭头通过哪个对象调用」。

箭头在普通方法执行期间创建时，外层方法已经拥有接收者。箭头在类实例字段初始化器中创建时，初始化上下文提供当前实例。箭头在模块顶层创建时没有对象接收者可继承，因此不要把浏览器脚本顶层的旧行为推广到模块。

`bind()` 作用于箭头函数时仍返回一个新函数，也仍能预置参数。只是绑定对象不会改变箭头解析到的 `this`。这会同时产生两个审查点：接收者没有改变，但函数标识已经改变。

### `arguments`、`super` 与 `new.target`

箭头函数没有自己的 `arguments`。嵌套在普通函数中时，它可以读取那个普通函数的 `arguments`；嵌套多层箭头也会继续向外查找。若目标是本次箭头调用的实参，剩余参数才是正确接口。

`super` 与 `new.target` 同样按外层环境解析。方法内部创建的箭头可以延续该方法允许的 `super` 访问，构造函数内部创建的箭头可以观察外层构造调用的 `new.target`。这不是箭头新增了绑定，而是它没有遮蔽外层绑定。

这种词法继承可以有意使用，但也会让机械重构产生隐蔽变化。普通函数原先读取自己的 `arguments` 或 `new.target`，改成箭头后可能读取另一层值而不是立即报错，因此测试必须覆盖嵌套调用。

### 严格模式与模块

箭头函数不会自动把未知接收者变成全局对象。它只使用外层 `this` 的实际值，而这个值受脚本类型、严格模式以及外层调用形式影响。把示例输出写成固定的全局对象或 `undefined`，却不说明运行上下文，通常是不可靠的。

ESM 顶层的 `this` 是 `undefined`。CommonJS 包装器、浏览器经典脚本与开发者工具控制台可能提供不同外层环境。可移植代码应让数据依赖来自参数或明确拥有者，而不是借助顶层 `this`。

## 可调用性、构造能力与函数形态

### 不可构造的可调用对象

箭头函数拥有调用能力，所以可以传给任何只要求可调用值的 API。它没有构造能力，对它执行 `new` 会在函数体运行前抛出 `TypeError`。函数内容是否给 `this` 赋值不会改变这一点。

箭头函数没有供实例继承的自身 `prototype` 属性，因为它不参与普通构造流程。不过，「没有自身 `prototype`」不是完整的构造能力检测：方法等其他函数形态也不可构造，而以可构造目标创建的绑定函数仍可转发构造。API 应直接规定是否接受构造器，不要靠一个属性猜测。

因为箭头不可构造，把首字母大写的构造函数重构成箭头尤其危险。静态检查应同时追踪定义与 `new` 调用，运行测试还应覆盖实际导出边界，避免只在定义文件中确认语法。

### 异步函数与生成器

箭头函数可以带 `async` 前缀。表达式函数体的值会成为返回 Promise 的兑现值，抛出的异常会成为拒绝原因；块函数体仍需要显式 `return`。`async` 不改变词法 `this` 或函数标识规则。

箭头函数不能写成生成器，也不能在自身函数体中直接使用 `yield`。需要生成器协议时使用 `function*` 或生成器方法。箭头内部更深一层的生成器拥有自己的函数体，这不等于箭头本身成为生成器。

### 原型与自有属性

普通函数作为构造器时，其 `prototype` 对象参与实例的原型链（prototype chain）。普通类方法也存放在类原型上，由实例共享。箭头函数字段则在每次实例初始化时创建并定义为自有属性。

这个差异首先是语义与标识问题。原型方法可以通过原型统一替换，并按调用形式接收实例；字段箭头在实例上遮蔽同名原型成员，并保留初始化时的实例。只有测量了实例数量与运行环境后，才适合进一步讨论资源成本。

继承也需要按属性查找顺序分析。基类初始化出的字段箭头可能遮蔽子类原型上的同名方法；派生类自己的同名字段又会在 `super()` 返回后初始化。依赖覆盖的设计应使用可观察调用测试，而不是仅检查类源码中是否出现同名成员。

## 方法、回调与生命周期

### 对象方法需要调用方接收者

对象字面量不会为其中的箭头函数字段创建新的 `this` 环境。属性访问能找到该函数，但随后的调用不能把对象安装为箭头的接收者。需要多态接收者时，简写方法语法表达得更准确。

并非所有对象属性上的箭头都错误。如果函数只依赖参数，或者明确需要对象外层作用域的 `this`，把它保存为属性可以是有意设计。审查重点是函数体依赖与调用契约是否一致，而不是属性右侧出现了哪种语法。

普通方法作为值传递后会失去原来的调用形式。可以传入 `value => object.method(value)`，也可以对方法绑定一次并保存结果。包装器能明确控制转发参数，绑定函数则保留目标与预置接收者；二者都会创建需要管理的新函数标识。

### 注册与注销使用同一标识

事件目标、消息总线和观察者 API 通常按函数对象标识移除处理器。两个独立求值的箭头不会因为函数体相同而相等。若 API 还比较捕获阶段等选项，注销时也必须复现那些匹配条件。

实例字段箭头天然提供一个可重复读取的函数标识，适合实例拥有的长期订阅。局部订阅也可以把箭头保存在 `const handler` 中，并在同一所有权范围内返回清理函数。关键是让创建、注册与注销共享一个清楚的生命周期。

重复调用 `start()` 还可能多次注册同一个或多个处理器。保存函数标识只能解决可注销性，不能自动提供幂等性。需要单次注册契约时，应另行保存订阅状态并测试重复启动。

### 参数转发

回调 API 可能传入比业务函数预期更多的参数。箭头包装器可以只转发所需值，避免数组方法的索引参数或事件 API 的额外信息意外进入目标函数。直接传递函数前，应先确认双方签名兼容。

剩余参数适合契约本来就接受可变实参数量的函数，不应作为无条件转发所有内容的借口。跨越日志、安全或序列化边界时，明确挑选参数更容易防止秘密和大型对象被传播。

## 选择与验证

### 按需求选择函数形态

| 需求 | 合适形态 | 原因 |
| --- | --- | --- |
| 短小纯转换 | 箭头函数 | 依赖由参数明确表达 |
| 方法内部保留实例的回调 | 箭头函数 | 词法使用外层方法的 `this` |
| 对象或类的动态方法 | 方法语法 | 调用形式提供接收者 |
| 构造实例 | 类或可构造普通函数 | 需要 `[[Construct]]` |
| 收集本次调用的可变实参 | 带剩余参数的函数 | 获得明确命名的数组 |
| 原型共享并允许覆盖 | 普通类方法 | 成员位于原型上 |

语法长度不是决策输入。先写出函数是否需要动态接收者、构造能力、稳定回调标识以及原型共享，再选择形态。这样重构审查可以比较契约，而不只是比较字符数。

### 测试真实调用形式

接收者测试必须复现生产调用表达式。直接执行 `object.method()` 无法证明该方法作为回调传递后仍工作；只执行箭头的普通调用，也无法证明 `call()` 不会改变其 `this`。至少覆盖属性调用、脱离调用和显式接收者调用中实际存在的路径。

返回值测试应同时覆盖表达式函数体与经过编辑的块函数体。对异步箭头要等待 Promise 并断言兑现值，避免只检查日志。返回对象时验证对象结构，可以及时发现括号或 `return` 在自动修改中丢失。

生命周期测试要保存处理次数，并在注销后再次触发事件。若支持重复启动，还要确认监听器数量不会增长。这个测试同时验证函数标识、幂等状态和清理所有权，比单独检查 `off()` 被调用更可靠。

构造能力测试应从公开导出执行真实的 `new` 调用。若接口接受普通回调而不接受构造器，也应测试错误输入如何报告。不要把 `prototype` 属性存在与否当作唯一断言。

<!-- /deep -->

[检查点: javascript/arrow-functions](https://codewiki.com/zh/javascript/arrow-functions/#checkpoint)

## 延伸阅读

- [MDN：箭头函数表达式](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions)
- [MDN：`this`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this)
- [MDN：剩余参数](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters)
