箭头函数

箭头函数用紧凑语法创建函数表达式,并从外层继承 this 等绑定;理解返回规则、调用语义与函数标识,才能安全地编写回调。

难度 进阶 时长 标准深度约 11分钟
版本 Node 24
what

箭头函数(arrow function) 是使用 => 写出的函数表达式。它可以采用表达式函数体并隐式返回结果,而且不创建自己的 thisargumentssupernew.target 绑定。

trap

箭头函数不是普通函数的全面简写。把对象方法、构造函数或依赖动态接收者的回调机械改成箭头函数,会改变程序语义。

fix

需要继承外层 this 的回调使用箭头函数;需要由调用方式决定 this、需要构造能力或需要自身 arguments 时,使用普通函数或方法。

是什么,为什么存在

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

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

箭头函数也没有自己的 argumentssupernew.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 模块顶层创建时,外层 thisundefined

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

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

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

示例

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

从表达式返回数据

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

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)}`);
[{"id":"A-17","label":"A-17: $125.00"},{"id":"C-03","label":"C-03: $42.50"}]
paid total: $167.50

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

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

保留创建位置的 this

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

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));
west: EUR 12.50
west: EUR 8.00
east: 8.00

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

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

区分外层 arguments 与本次实参

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

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')));
{"label":"batch","factoryArguments":["batch",99],"values":["A","B"],"valuesAreArray":true}

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

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

用类字段保存回调

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

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);
first:1
first:2
true
false

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

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

陷阱

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

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

在块函数体中漏掉 return

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

用新箭头函数注销回调

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

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

修复方法: 重构前搜索 newprototypethisargumentssupernew.target 的所有用法。需要本次调用实参时改成剩余参数;需要构造或动态接收者时保留普通函数、方法或类。

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

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

深入 语法边界与解析

语法边界与解析

参数列表

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

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

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

表达式函数体与块函数体

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

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

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

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

名称与递归

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

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

词法上下文

this 的来源

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

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

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

argumentssupernew.target

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

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

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

严格模式与模块

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

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

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

不可构造的可调用对象

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

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

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

异步函数与生成器

箭头函数可以带 async 前缀。表达式函数体的值会成为返回 Promise 的兑现值,抛出的异常会成为拒绝原因;块函数体仍需要显式 returnasync 不改变词法 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 属性存在与否当作唯一断言。

延伸阅读

检查点

4个问题 · 2 道输出预测题 · 1 道找错题

复制为 Markdown 面试题库 在 GitHub 上编辑 报告错误 讲清楚了吗?