1. 先别急着用箭头函数:先搞懂 this 为什么会丢
在 JavaScript 开发里,有一个场景几乎每个前端都遇到过:写了一个对象方法,内部用setTimeout或者addEventListener回调,结果方法里的this突然就变成了undefined,或者指向了window(严格模式下是undefined)。
很多人的第一反应是“用箭头函数就好”。确实,箭头函数能解决这个问题,但如果你不清楚this为什么会丢,箭头函数也只会变成另一种“背答案”式的写法。换个场景,换成Vue的methods、React类组件的自定义方法,你又可能踩进去。等到面试官问一句“箭头函数的 this 到底是什么时候绑定的”,很多人又卡壳了。
这篇文章不打算只给你一份“箭头函数写法大全”。我们从真正的底层机制出发,拆解this的绑定规则,理解箭头函数为什么能“锁住”this,再用完整的代码示例演示实际开发中的典型场景和验证方法。读完之后,你不仅能解决 this 丢失问题,还能举一反三,看懂各种奇怪 this 指向背后的原因。
2. this 到底是什么:调用约定,而不是定义位置
2.1 this 不是函数自身的属性
很多新手会误以为this指向函数定义时的某个对象,比如“这个方法写在 obj 里,所以 this 就应该指向 obj”。这是一个很有迷惑性的直觉,但 JavaScript 的this不是这样工作的。
this是函数执行时的上下文对象,它的指向由调用方式决定,也就是我们常说的“调用点”(call-site)。在绝大多数情况下,你只需要看这个函数是怎么被调用的,就能判断this指向谁。
看一个最简单也最容易“骗人”的例子:
const user = { name: '张三', greet: function() { console.log(`你好,我是${this.name}`); } }; user.greet(); // 你好,我是张三 const fn = user.greet; fn(); // 报错或输出 undefined(取决于是否严格模式)同样的函数,user.greet()调用时this指向user;把函数赋值给fn后直接调用fn(),this就指向了全局对象(非严格模式)或undefined(严格模式)。函数本身没变,变的只是调用方式。
2.2 调用点决定 this:四类绑定规则
JavaScript 的this绑定规则可以归纳成四类,按优先级从低到高排列:
| 绑定规则 | 触发场景 | this 指向 |
|---|---|---|
| 默认绑定 | 直接调用fn() | 非严格模式指向全局对象;严格模式指向undefined |
| 隐式绑定 | 对象方法调用obj.fn() | 指向该对象 |
| 显式绑定 | fn.call(ctx)/fn.apply(ctx)/fn.bind(ctx) | 指向传入的ctx |
| new 绑定 | new Fn() | 指向新创建的对象 |
这里要注意一个容易忽略的点:隐式绑定中的“对象方法调用”指的是调用那一瞬间的表达形式。obj.fn()中间的obj.就是隐式绑定生效的关键。如果你先把obj.fn取出来再调用,隐式绑定就丢了。
const obj = { name: '对象A', show: function() { console.log(this.name); } }; const rawFn = obj.show; rawFn(); // 失去隐式绑定,this 指向全局对象(非严格模式)2.3 常见 this 丢失的几种场景
实际开发中,this 丢失最典型的场景有下面几类:
第一,回调函数。把方法作为回调传给setTimeout、addEventListener、Array.prototype.forEach等 API 时,函数并不会以“对象的方法”形式被调用,默认绑定就会生效。
const counter = { count: 0, start: function() { setInterval(function() { this.count++; console.log(this.count); // NaN 或 undefined 相关错误 }, 1000); } }; counter.start(); // 这里 this 不再指向 counter第二,React 类组件中的事件处理函数。事件绑定把函数引用传给了底层 DOM 系统,触发时函数是“裸调用”,this自然就丢了。所以早期的 React 类组件中,你经常能看到this.handleClick = this.handleClick.bind(this)这种写法。
第三,解构赋值。从对象中解构方法出来再调用,隐式绑定同样会丢失。
const actions = { type: 'click', run: function() { console.log(this.type); } }; const { run } = actions; run(); // undefined理解这些场景之后,再来看箭头函数为什么是“对症下药”的解法。
3. 箭头函数的核心机制:词法 this,而不是运行时 this
3.1 箭头函数没有自己的 this
箭头函数最根本的一条规则是:它没有属于自己的 this 绑定。它不是不绑定 this,也不是把 this 绑定到某个固定对象,而是在定义箭头函数时,捕获外层作用域的this值。
换句话说,箭头函数内部的this就是“写代码时所在的那个作用域里的 this”,与函数如何被调用无关。
对比一下两个例子:
// 普通函数:调用方式决定 this function regularFunc() { console.log(this); // 取决于调用方式 } // 箭头函数:定义位置决定 this const arrowFunc = () => { console.log(this); // 取决于定义箭头函数时外层作用域的 this };如果箭头函数定义在全局作用域,那么它的this就是全局对象(非严格模式下)或undefined(模块环境)。如果定义在某个函数内部,它的this就继承自那个函数的this。
3.2 箭头函数不是“语法糖”:用代码说明差异
很多初学者会把箭头函数当成普通函数的简写形式,这是最大的误区。箭头函数和普通函数至少有四个本质差异:
- 没有自己的
this、arguments、super、new.target。 - 不能作为构造函数,不能被
new调用。 - 没有
prototype属性。 arguments对象不绑定,如果使用arguments会向外层作用域查找。
先看arguments的差异:
function outer() { const innerArrow = () => { console.log(arguments); // 取自 outer 的 arguments }; innerArrow(); } outer(1, 2, 3); // Arguments(3) [1, 2, 3] const standaloneArrow = () => { console.log(arguments); // 如果箭头函数在全局作用域定义,这里会报错(ReferenceError) }; // standaloneArrow();再看this的差异:箭头函数的this在定义时就已经固定了,后续无论怎么调用都不会变。普通函数的this每次调用都可能不同。
const obj = { name: '箭头函数测试', normal: function() { return function() { console.log(this); }; }, arrow: function() { return () => { console.log(this); }; } }; const normalResult = obj.normal(); normalResult(); // 全局对象或 undefined(严格模式) const arrowResult = obj.arrow(); arrowResult(); // obj,因为箭头函数定义在 obj.arrow 的作用域内obj.arrow被调用时,它的this指向obj(隐式绑定),箭头函数定义在这个作用域内,所以箭头函数的this也指向obj。
4. 环境准备与前置知识:从语法到运行环境
4.1 运行环境要求
箭头函数是 ES6(ECMAScript 2015)引入的语法。现代浏览器和 Node.js 都已经原生支持,不需要额外配置。
如果是老项目需要兼容旧浏览器(例如 IE11 及更低版本),需要借助 Babel 做语法转换,转换后的代码会变成普通函数加var _this = this的方式,这正是箭头函数“捕获 this”的机制本质。
4.2 Node.js 环境准备
本文所有代码都可以在 Node.js 环境中直接运行。建议使用 Node.js 14 或更高版本(版本请以实际项目为准,本文重点演示通用思路)。检查环境:
node -v如果已经安装了 Node.js,直接用node命令运行单个 JS 文件即可:
node arrow-function.js浏览器环境下,直接在 DevTools 的 Console 面板中粘贴代码也能验证。
4.3 关于严格模式的提醒
这里有一个可能影响this指向的隐藏因素:严格模式。
普通函数在非严格模式下被“裸调用”,this指向全局对象(浏览器中的window,Node.js 中的global);在严格模式下则指向undefined。箭头函数不区分严格模式和非严格模式,它的this完全由外层作用域决定,所以不会出现这种“环境变化导致 this 变化”的情况。
'use strict'; function strictFn() { console.log(this); // undefined } strictFn(); const arrow = () => { console.log(this); // 取决于外层作用域,如果由模块加载,通常是 undefined 或 module.exports 等 }; arrow();5. 箭头函数的 this 绑定原理:模拟编译过程看清本质
5.1 Babel 转换后的代码
要理解箭头函数为什么能“锁住” this,最好的办法是看看 Babel 把箭头函数转换成 ES5 代码后的样子。
原始代码:
const obj = { name: '测试', getArrow: function() { return () => { console.log(this.name); }; } };Babel 转换后大致是:
var _this = this; var obj = { name: '测试', getArrow: function() { var _this2 = this; return function() { console.log(_this2.name); }; } };转换后的代码很直观:箭头函数内部的this被替换成了外层作用域里保存的一个普通变量。这就是“词法 this”的底层实现方式。箭头函数本身没有 this,它只是借用了外层作用域里那个已经绑定好的 this 值。
理解这一点之后,很多问题都变得清晰:为什么箭头函数不能用call、apply、bind改变 this?因为转换之后,函数内部根本没有this标识符,你传入的上下文被完全忽略。
const arrow = () => { console.log(this); }; const target = { name: '尝试修改' }; arrow.call(target); // 仍然输出外层 this,target 被忽略5.2 多层嵌套:箭头函数 this 的查找规则
箭头函数内部的 this 是按“词法作用域链”一层层向外查找的。如果有多个嵌套的箭头函数,最终找到的是最近的一层非箭头函数作用域的 this。
const obj = { name: '最外层', outer: function() { const innerArrow = () => { const innermostArrow = () => { console.log(this.name); }; innermostArrow(); }; innerArrow(); } }; obj.outer(); // 最外层这里的innermostArrow向上查找,找到的第一个“非箭头函数作用域”是outer函数,而outer被调用时this指向obj,于是输出最外层。
5.3 箭头函数不能改变 this 的隐藏含义
因为箭头函数没有自己的 this,所以call、apply、bind对它的 this 是无效的。但需要注意:参数仍然会被传递。
const add = (a, b) => a + b; console.log(add.call(null, 2, 3)); // 5,参数正常传递 console.log(add.apply(null, [4, 5])); // 9 const boundAdd = add.bind({}, 10, 20); console.log(boundAdd()); // 30,参数被绑定,this 被忽略这个容易被忽略的特性在某些框架底层代码里会引发奇怪现象,值得记住。
6. 实际开发中的典型场景:箭头函数怎么用才正确
6.1 场景一:定时器回调
回到文章开头的问题。用箭头函数解决定时器中的 this 丢失:
const counter = { count: 0, start: function() { setInterval(() => { this.count++; console.log(this.count); }, 1000); } }; counter.start(); // 1 // 2 // 3 // ...箭头函数定义在start函数的作用域内,start被以counter方法的形式调用,this正确指向counter,因此定时器回调里的this也指向counter。
6.2 场景二:事件监听
DOM 事件绑定时,普通函数作为事件监听器,this会指向触发事件的 DOM 元素,但有时我们反而希望this指向组件实例或某个业务对象:
class ButtonHandler { constructor(button, message) { this.message = message; button.addEventListener('click', () => { this.handleClick(); }); } handleClick() { console.log(this.message); } } const button = document.getElementById('btn'); const handler = new ButtonHandler(button, '按钮被点击了');这里箭头函数捕获的是constructor中this的值,也就是新建的ButtonHandler实例。
需要特别提醒的是:如果需要使用事件对象来获取event.target之类的信息,箭头函数可以照常接收事件参数,只是 this 不会再指向 DOM 元素。
button.addEventListener('click', (event) => { console.log(event.target); // DOM 元素本身,正常获取 console.log(this); // 外层 this,不是 DOM 元素 });6.3 场景三:数组方法中的回调
map、filter、forEach等数组方法,常常需要在外层对象方法的上下文里访问this:
const cart = { items: [ { name: '键盘', price: 199 }, { name: '鼠标', price: 99 } ], getTotal: function() { return this.items.reduce((total, item) => { return total + item.price; }, 0); }, getNames: function() { return this.items.map((item) => item.name); } }; console.log(cart.getTotal()); // 298 console.log(cart.getNames()); // [ '键盘', '鼠标' ]如果不使用箭头函数,map回调里的普通函数的this会丢失(除非你给map传入第二个参数指定 thisArg)。
关于Array.prototype.map的第二个参数thisArg,值得多说一句。数组方法的回调如果使用普通函数,可以通过第二个参数显式传入 this:
const handler = { prefix: '商品: ', formatNames: function() { return this.items.map(function(item) { return this.prefix + item.name; }, this); // 第二个参数把 this 传入 } };这个方案在很多场景下也能用。但箭头函数让代码更简洁直观,不需要额外的参数传递,也降低了出错概率。
6.4 场景四:类方法中的自定义方法
除了 React 类组件,在普通的 JavaScript 类里也会遇到:
class Counter { constructor() { this.value = 0; } increase() { this.value++; console.log(this.value); } // 用箭头函数定义类字段(需要支持 Class Fields 语法的环境) autoIncrease = () => { this.value++; console.log(this.value); } } const counter = new Counter(); const rawIncrease = counter.increase; const rawAutoIncrease = counter.autoIncrease; rawIncrease(); // 报错,this 是 undefined rawAutoIncrease(); // 正常运行,this 指向 counter 实例这里要注意:类字段使用箭头函数 = () => {}的写法依赖较新的语法特性(Class Fields),在 Node.js 12+ 和现代浏览器中已经支持。但它有一个陷阱:每个实例都会创建一个新的箭头函数,不像类方法那样共享同一个原型方法。如果创建大量实例,内存开销会更高。因此这个写法适合“确实需要绑定 this”的场景,不宜滥用。
6.5 场景五:构造函数和对象字面量方法
箭头函数不能作为构造函数,也不应该用于需要动态 this 的对象方法。
const obj = { name: '对象', greet: () => { console.log(`你好,${this.name}`); } }; obj.greet(); // 你好,undefined这里箭头函数定义在对象字面量中,但它捕获的是外层作用域的this(在模块环境中通常是undefined或全局对象),并不是obj。所以对象字面量中的方法不要用箭头函数,除非你明确知道自己想要的是外层 this。
构造函数同样不能用箭头函数:
const Person = (name) => { this.name = name; }; // new Person('张三'); // 直接报错:Person is not a constructor7. 代码运行与验证:从执行结果反推绑定逻辑
7.1 完整示例文件
创建一个this-demo.js文件,写入以下内容:
// 文件路径:this-demo.js 'use strict'; const moduleScope = this; function demonstrate() { const self = this; function normalFunction() { console.log('普通函数 this:', this); } const arrowFunction = () => { console.log('箭头函数 this:', this); }; return { normal: normalFunction, arrow: arrowFunction, callBoth: function() { normalFunction(); // 默认绑定:严格模式下 this 为 undefined arrowFunction(); // 词法绑定:this 为 demonstrate 的 this } }; } const demoObject = { label: 'demoObject' }; const result = demonstrate.call(demoObject); result.normal(); // 普通函数 this: undefined result.arrow(); // 箭头函数 this: { label: 'demoObject' } result.callBoth(); // 普通函数 this: undefined // 箭头函数 this: { label: 'demoObject' }运行:
node this-demo.js重点看输出结果:result.normal()是被“取出来”之后直接调用的,普通函数的 this 在严格模式下是undefined;result.arrow()的箭头函数虽然也在同样的位置被取出调用,它的 this 却依然是demoObject,因为箭头函数的 this 在定义时就已经确定为demonstrate.call(demoObject)传入的那个对象。
这组对比能够直观验证:普通函数看“调用瞬间”,箭头函数看“定义时的外层作用域”。
7.2 验证 call、apply、bind 对箭头函数无效
再创建一个this-ignore.js:
// 文件路径:this-ignore.js const objA = { name: '对象A' }; const objB = { name: '对象B' }; const arrow = () => { console.log('箭头函数 this 的 name:', this.name); }; const regular = function() { console.log('普通函数 this 的 name:', this.name); }; arrow.call(objA); // 箭头函数 this 的 name: undefined regular.call(objA); // 普通函数 this 的 name: 对象A const boundArrow = arrow.bind(objB); boundArrow(); // 仍然 undefined const boundRegular = regular.bind(objB); boundRegular(); // 对象B这里要注意:在 Node.js 的一个模块中,this默认指向module.exports(一个空对象),所以this.name是undefined。如果是浏览器全局环境,this指向window,输出结果会变成window上的某个属性值。这正说明了箭头函数 this 的“外层来源”随环境而变化。
8. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 普通函数在严格模式下裸调用,this 是 undefined,导致读取属性报错 | 默认绑定规则生效 | 检查函数调用处是否有.运算符 | 改用箭头函数;或者使用fn.call(ctx)显式绑定 |
| 对象方法中使用箭头函数,this 没有指向对象 | 箭头函数捕获的是外层作用域 this,而不是对象自身 | 查看对象字面量所在作用域 | 对象方法使用普通函数,需要保留动态 this 时不要用箭头函数 |
| 定时器回调里使用普通函数,this 不是预期对象 | 回调函数被定时器系统直接调用,默认绑定生效 | 在回调第一行打印 this 确认指向 | 回调改为箭头函数 |
| 箭头函数无法用 call 修改 this,导致接口调用异常 | 箭头函数本身没有 this,call 传入上下文被忽略 | 检查函数是否用箭头函数定义 | 需要动态 this 的函数改用普通函数 |
| class 中方法用箭头函数,每个实例占用额外内存 | 类字段箭头函数会在每个实例创建新函数 | 打印counter.increase === counter.autoIncrease观察 | 只在必须绑定的场景使用;普通场景用类方法 + 构造器 bind |
| 事件监听回调中 this 不是预期对象 | 普通函数作为事件监听器 this 指向 DOM 元素 | 在回调内打印 this | 用箭头函数保留外层 this;但要确认不依赖 DOM this |
| Vue 选项 API 的 methods 方法中用普通函数嵌套函数,this 变成 undefined | 嵌套函数默认绑定 | 检查嵌套函数是否用箭头函数 | 嵌套回调优先使用箭头函数 |
| React 类组件中自定义方法调用报 undefined | 事件绑定导致方法失去绑定 | 查看 render 方法的 JSX 绑定时写的是this.fn还是fn | 构造器中 bind,或者使用箭头函数声明类方法 |
9. 箭头函数与普通函数的选型判断
既然箭头函数解决了很多问题,为什么不全部用箭头函数?这里要给一个明确的选型建议。
适合使用箭头函数的场景:
- 回调函数,尤其是定时器、事件监听、Promise 链式操作。
- 数组方法回调(map、filter、reduce、forEach)。
- 需要在嵌套函数里访问外层 this 的效果。
- 函数式编程风格中作为纯函数传递。
不应该使用箭头函数的场景:
- 对象字面量方法,需要动态 this 时。
- 构造函数,箭头函数无法 new。
arguments需要动态绑定到当前函数的场景。- 需要通过
call、apply、bind动态控制 this 的库函数(比如某些事件处理器)。 - 定义类方法方法,需要共享原型方法与性能优化时。
一个更准确的说法是:你选择箭头函数,不是因为“箭头更短”,而是因为“不需要自己的 this”。把这一点想清楚,选型就不再靠死记硬背。
10. 最佳实践与工程建议
10.1 命名与代码可读性
箭头函数省略了function关键字,代码变短的同时也降低了部分读者的阅读速度。如果团队中有刚接触 ES6 的开发者,建议在代码规范中明确:箭头函数多用于回调与简单函数表达式;逻辑复杂的函数仍然建议使用命名函数,或使用箭头函数赋值给常量,让函数有名字容易调试。
// 建议:给箭头函数一个变量名,便于堆栈追踪 const formatUser = (user) => { return `${user.name} (${user.email})`; };10.2 团队规范:统一约定 this 的绑定方式
在一个多人协作的项目里,最怕的是“同一类场景,有人用箭头函数,有人用 bind,有人用临时变量保存 this”。这会导致代码风格割裂,也增加了排查问题的成本。
建议团队约定:
- 回调中需要外层 this 时,优先使用箭头函数。
- 类组件中需要保证方法 this 绑定时,统一使用类字段箭头函数或构造器 bind,不要混用。
- 不推荐在类方法内部写
var self = this这类临时变量,除非项目需要兼容非常老的环境。
10.3 严格模式与 ESLint
开启 ESLint 的no-this-assignment或prefer-arrow-callback规则,能帮助团队在代码审查前发现常见 this 问题。但同时要配合class-methods-use-this规则,避免因为不访问 this 的方法被误判。
ESLint 配置示例:
{ "rules": { "prefer-arrow-callback": "error", "no-this-assignment": "error" } }prefer-arrow-callback会在可能的情况下强制使用箭头函数替代function表达式,no-this-assignment会禁止var self = this这类写法,迫使开发者使用更符合 es6 风格的方式。
10.4 TypeScript 项目中的额外提醒
如果你在 TypeScript 项目中大量使用箭头函数,有一个类型层面的差异需要记住:普通函数声明和箭头函数声明的类型并不完全兼容。箭头函数类型适合作为回调签名处理,但这不影响 this 的运行时绑定机制。不过,TS 的noImplicitThis标志能帮助在编译阶段捕获错误的 this 推断。
interface Counter { count: number; increment: () => void; } const counter: Counter = { count: 0, increment: function() { this.count++; } };这段代码中increment方法如果是普通函数,this类型能够正确推断;如果写成箭头函数,反而可能因为捕获外层 this 导致类型复杂化。TS 项目里尤其需要区分“方法的 this 动态绑定”和“回调的 this 词法绑定”两种场景。
10.5 性能与内存
箭头函数的运行时性能与普通函数基本没有差别,不必担心性能开销。唯一需要注意的是类字段箭头函数在实例上的使用:每个实例创建独立的函数对象,会占用更多内存,也会导致原型链上无法覆盖方法。如果类实例数量很大,建议还是使用普通方法加构造器 bind:
class MyClass { constructor() { this.handleClick = this.handleClick.bind(this); } handleClick() { // ... } }10.6 调试与日志记录
调试 this 相关问题时,尽快在函数第一行输出this的值,或者使用调试工具在函数调用处打断点。因为 this 是在调用时确定的,只看函数定义往往无法找到问题。检查调用堆栈时,重点看“是谁调用了这个函数”,就能快速定位默认绑定是否接管了 this。
11. 总结与后续学习方向
这篇文章从 what、why、how 三个层面拆解了 JS 箭头函数与 this 的关系:
- this 不是定义时决定的,而是调用时决定的,四类绑定规则按优先级排列。
- 箭头函数没有自己的 this,定义时捕获外层作用域的 this,并且一旦确定就不能通过 call / apply / bind 改变。
- 实际开发中,定时器回调、事件监听、数组方法回调、类方法绑定是箭头函数的典型应用场景;而对象方法和构造函数必须继续使用普通函数。
- 验证 this 指向的最好方法是多写对比代码,在 Node.js 和浏览器环境中分别观察结果,理解“模块环境 this 与全局环境 this”的差异。
下一步建议:
如果还想继续深入,可以从这几个方向延伸:
- 阅读 ECMAScript 规范中关于
LexicalThis的章节,理解词法环境的正式定义。 - 学习
bind的底层实现原理,模拟一个简易版 bind,深入理解显式绑定的过程。 - 在 React 和 Vue 的双框架中分别实践 this 的绑定与丢失场景,积累框架层面的坑位清单。
- 尝试把一段遗留代码里的
var self = this全部改为箭头函数,再运行测试,观察行为是否一致,这是检验你对 this 理解是否到位的好方法。
希望这篇文章能帮你摆脱“this 到底指向谁”的困惑。实际开发中遇到 this 问题时,建议先画一遍调用链,再判断应该用普通函数还是箭头函数。顺手收藏这篇文章,下次再遇到类似 bug 时可以直接翻出来对照排查。