写 JS 代码的时候,你有没有被this坑过?
明明在setTimeout回调里只是想访问一下组件实例的数据,结果this.name直接给你报undefined;明明在事件监听器里想拿到点击的元素,结果this指向了window。这种问题在我刚开始写前端时几乎天天遇到,排查半天最后发现,不就是回调函数里的this丢了吗?
后来 ES6 普及,箭头函数大量出现在代码里,不少同学说“箭头函数好用,this不会丢了”。这句话对了一半:箭头函数确实能解决一部分this丢失问题,但它背后的机制并不是“把 this 绑定到了当前对象”,而是箭头函数根本没有自己的 this。理解不了这层区别,你会在一堆新代码里写出更隐蔽的 bug。
这篇文章我用「问题 → 原理 → 写法 → 场景 → 坑位 → 实践」这条线,彻底讲清楚 JS 箭头函数和this的关系。看完之后你再遇到回调函数里的this问题,应该能直接说出原因,而不只是换个写法碰运气。
1. 这篇文章真正要解决的问题
在开始讲原理之前,先看一个几乎所有前端都写过的代码:
const user = { name: '张三', greet: function() { setTimeout(function() { console.log(`你好,我是 ${this.name}`); }, 1000); } }; user.greet(); // 输出:你好,我是 undefined为什么这里this.name拿不到user.name?
因为setTimeout里的回调函数是普通函数,它被调用时,this指向的是全局对象(浏览器里是window,Node.js 里是global),而不是外层的user对象。
传统解决方案是先把this存下来:
const user = { name: '张三', greet: function() { const that = this; setTimeout(function() { console.log(`你好,我是 ${that.name}`); }, 1000); } };或者用bind:
const user = { name: '张三', greet: function() { setTimeout(function() { console.log(`你好,我是 ${this.name}`); }.bind(this), 1000); } };这两种写法都能解决,但都显得笨重。箭头函数出现后,直接这么写:
const user = { name: '张三', greet: function() { setTimeout(() => { console.log(`你好,我是 ${this.name}`); }, 1000); } };完美输出你好,我是 张三。
所以这篇文章要解决的核心问题就是:
- 普通函数的
this是怎么确定的? - 箭头函数的
this为什么“不丢”? - 箭头函数到底能不能完全替代普通函数?
- 实际项目里应该怎么选?
下面逐一拆开讲。
2. 普通函数:this 在调用时确定
要理解箭头函数,先得理解普通函数的this机制。
JavaScript 里有一个核心规则:普通函数的this是在函数被调用时确定的,而不是在定义时确定的。这个“调用时确定”听起来抽象,实际上可以归纳成四种绑定规则。
2.1 默认绑定
独立调用函数,没有任何修饰,this指向全局对象。严格模式下指向undefined。
function show() { console.log(this); } show(); // 浏览器环境下输出 windowfunction showStrict() { 'use strict'; console.log(this); } showStrict(); // undefined2.2 隐式绑定
函数作为某个对象的方法被调用,this指向这个对象。
const obj = { name: '张三', show: function() { console.log(this.name); } }; obj.show(); // 张三但隐式绑定有一个非常经典的坑:把方法取出来单独调用,this 就丢了。
const obj = { name: '张三', show: function() { console.log(this.name); } }; const fn = obj.show; fn(); // undefined,this 指向全局对象这正好解释了第一节里user.greet()执行时,为什么内部setTimeout回调拿不到this.name:那个回调函数是独立调用的,不是作为user的方法调用的。
2.3 显式绑定
通过call、apply、bind手动指定this。
function show() { console.log(this.name); } const obj = { name: '张三' }; show.call(obj); // 张三 show.apply(obj); // 张三 const boundShow = show.bind(obj); boundShow(); // 张三call和apply的区别只在传参方式:call逐个传,apply传数组。bind比较特殊,它返回一个新函数,这个新函数的this永久绑定到传入的对象。
2.4 new 绑定
使用new调用函数时,this指向新创建的对象。
function Person(name) { this.name = name; } const person = new Person('张三'); console.log(person.name); // 张三2.5 四种规则对比
| 绑定规则 | 调用方式 | this 指向 |
|---|---|---|
| 默认绑定 | 独立调用 | 全局对象 / undefined(严格模式) |
| 隐式绑定 | 作为对象方法调用 | 调用该方法的对象 |
| 显式绑定 | call / apply / bind | 手动传入的对象 |
| new 绑定 | new 调用 | 新创建的对象 |
普通函数还有一个特点:调用方式一变,this 就可能变。这既是灵活,也是混乱的根源。
3. 箭头函数:this 在定义时确定
箭头函数不是普通函数的语法糖,它在this的处理上有一个本质区别:箭头函数没有自己的 this。
这句话怎么理解?
当你在箭头函数内部访问this时,JavaScript 引擎不会为箭头函数单独绑定一个this,而是直接沿作用域链向上查找,找到最近一层非箭头函数作用域的this。如果一直找到全局作用域,那就是全局对象。
这种机制叫词法 this(lexical this),意思是this的取值规则由代码书写位置决定,和函数怎么调用无关。
3.1 定义时确定的核心逻辑
const obj = { name: '张三', greet: function() { setTimeout(() => { console.log(this.name); }, 1000); } }; obj.greet(); // 张三这里的箭头函数没有自己的this,它向上找到了greet函数作用域里的this。而greet是普通函数,作为obj的方法调用,所以它的this指向obj。箭头函数拿到的就是obj。
再看一个对比:
const obj = { name: '张三', greet: () => { console.log(this.name); } }; obj.greet(); // undefined为什么这里不行?
因为greet自己就是箭头函数,它没有自己的this,只能向上找。obj不是函数作用域,再向上就是全局作用域。全局作用域里没有name,所以输出undefined。
这里就是很多人踩坑的地方:箭头函数作为对象方法,拿不到对象实例的 this。因为它根本没打算给你绑定对象。
3.2 call / apply / bind 无法改变箭头函数的 this
普通函数可以通过显式绑定改变this,但箭头函数不行。因为它根本没有自己的this,call、apply、bind传进去的对象会被直接忽略。
const arrow = () => { console.log(this.name); }; arrow.call({ name: '张三' }); // undefined arrow.apply({ name: '张三' }); // undefined const boundArrow = arrow.bind({ name: '张三' }); boundArrow(); // undefined从材料里的热词也能看到,“js箭头函数”、“this”、“箭头函数写法”一直是搜索高频词,说明大家对这里的理解确实有难度。关键在于记住一句话:箭头函数的 this 是定义时从外层作用域“继承”下来的,与调用方式无关。
4. 箭头函数的基本语法与写法
理解原理之后,再看语法就很轻松了。箭头函数的写法比普通函数简洁很多,但不同场景下要选对格式。
4.1 基础写法
// 普通函数 function add(a, b) { return a + b; } // 箭头函数完整写法 const add = (a, b) => { return a + b; }; // 箭头函数简写(单行表达式自动返回) const add = (a, b) => a + b;单行表达式写法会把表达式的结果作为返回值,这个特性配合数组的高阶函数非常好用。
4.2 一个参数时省略括号
const double = (x) => x * 2; const double = x => x * 2;两种写法等价。但当参数是 0 个或多个时,括号不能省略。
const getRandom = () => Math.random(); const sum = (a, b) => a + b;4.3 返回对象字面量要注意
箭头函数直接返回对象字面量时,必须加括号,否则解析器会把{}当成函数体。
// 错误写法:花括号被解析成函数体 const getObj = () => { name: '张三', age: 18 }; // 正确写法:外层加括号 const getObj = () => ({ name: '张三', age: 18 });这个细节很容易被忽略,特别是在写 React 组件返回配置对象的时候。
4.4 与普通函数写法对比
| 场景 | 普通函数 | 箭头函数 |
|---|---|---|
| 无参数 | function() { return 1; } | () => 1 |
| 一个参数 | function(x) { return x * 2; } | x => x * 2 |
| 多个参数 | function(a, b) { return a + b; } | (a, b) => a + b |
| 多行逻辑 | function(x) { const y = x + 1; return y; } | (x) => { const y = x + 1; return y; } |
| 返回对象 | function() { return { name: '张三' }; } | () => ({ name: '张三' }) |
5. 箭头函数的典型应用场景
箭头函数的优势集中在“短函数 + 回调”场景。判断标准是:你不需要函数自己的 this,你只需要沿用外层 this。
5.1 定时器与事件回调
这是最经典的应用场景。
function Timer() { this.seconds = 0; setInterval(() => { this.seconds++; console.log(this.seconds); }, 1000); } const timer = new Timer();如果用普通函数,需要bind(this)才能让回调里的this指向Timer实例。用箭头函数,直接沿外层作用域取到Timer实例的this。
5.2 数组高阶函数
map、filter、reduce、forEach这些方法,回调函数通常很短,用箭头函数可以让语义更聚焦。
const numbers = [1, 2, 3, 4, 5]; // 过滤出偶数 const evens = numbers.filter(n => n % 2 === 0); console.log(evens); // [2, 4] // 每个数乘以 2 const doubled = numbers.map(n => n * 2); console.log(doubled); // [2, 4, 6, 8, 10] // 求和 const total = numbers.reduce((sum, n) => sum + n, 0); console.log(total); // 15在这个场景里,基本上不会遇到this问题,因为回调本身不需要自己的this,箭头函数让代码更简洁。
5.3 Promise 链与异步回调
写异步代码时,箭头函数能避免this丢失。
const service = { data: [], load: function() { fetch('/api/data') .then(response => response.json()) .then(data => { this.data = data; console.log(this.data); }) .catch(error => { console.error(error); }); } }; service.load();这里的this.data = data如果写在普通函数里,this就不会指向service。箭头函数让异步回调整链都保持着外层的this。
5.4 Vue 或 React 组件中的实践
在 Vue 组件的methods里使用箭头函数要非常谨慎,但在定时器、事件监听、异步回调用箭头函数则很自然。
// Vue 组件示例 export default { data() { return { count: 0 }; }, methods: { start() { // 这里是方法,this 指向组件实例 setInterval(() => { // 箭头函数沿用上面普通方法的 this this.count++; }, 1000); } } };在 React 类组件中,箭头函数常用于绑定事件处理函数:
class Counter extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } // 箭头函数写法,this 指向组件实例 handleClick = () => { this.setState({ count: this.state.count + 1 }); }; render() { return ( <button onClick={this.handleClick}> 点击次数:{this.state.count} </button> ); } }这种用类属性加箭头函数的方式,在 React 类组件里非常常见,比在构造函数里逐个bind要省事得多。
5.5 封装高阶函数
箭头函数返回函数的写法,非常适合封装通用逻辑。
const withPrefix = (prefix) => (name) => `${prefix}_${name}`; const addUserPrefix = withPrefix('user'); console.log(addUserPrefix('张三')); // user_张三 console.log(addUserPrefix('李四')); // user_李四这里withPrefix接收一个前缀,返回一个新函数,新函数接收名字,返回带前缀的结果。箭头函数的简洁语法让这类代码写起来非常舒服。
6. 箭头函数不能乱用的场景
箭头函数确实好用,但它不是万能的。很多项目里的this问题,恰恰是箭头函数用错了位置造成的。下面这些场景建议继续使用普通函数。
6.1 对象方法
前面已经演示过,箭头函数作为对象方法是拿不到对象实例的this的。
const user = { name: '张三', greet: () => { console.log(`你好,我是 ${this.name}`); } }; user.greet(); // undefined如果需要通过this访问对象自身的数据,必须用普通函数:
const user = { name: '张三', greet: function() { console.log(`你好,我是 ${this.name}`); } }; user.greet(); // 张三6.2 原型方法
同样的问题也出现在原型方法里。
function Person(name) { this.name = name; } // 错误:无法访问实例属性 Person.prototype.greet = () => { console.log(`你好,我是 ${this.name}`); }; const person = new Person('张三'); person.greet(); // undefined // 正确:使用普通函数 Person.prototype.greet = function() { console.log(`你好,我是 ${this.name}`); }; person.greet(); // 张三6.3 需要动态 this 的 DOM 事件监听
在 DOM 事件监听中,普通函数的this会指向触发事件的元素,这是一个很实用的特性。
// 普通函数:this 指向被点击的按钮 button.addEventListener('click', function() { console.log(this); // button 元素 }); // 箭头函数:this 沿用外层作用域 button.addEventListener('click', () => { console.log(this); // window 或其他外层 this });如果你需要拿到触发事件的元素,普通函数反而更合适。箭头函数会把外层的this带进来,这在某些场景反而不方便。
6.4 构造函数
箭头函数不能用作构造函数。它没有自己的this,也就无法为新对象初始化属性。
const Person = (name) => { this.name = name; }; // TypeError: Person is not a constructor const person = new Person('张三');6.5 arguments 对象
普通函数的arguments对象可以直接访问参数,但箭头函数没有arguments。如果你要在函数内部访问参数列表,有两种选择:使用普通函数,或者用 rest 参数。
// 普通函数 function logParams() { console.log(arguments); } logParams(1, 2, 3); // Arguments(3) [1, 2, 3] // 箭头函数中 arguments 不存在 const arrowLog = () => { console.log(arguments); // ReferenceError: arguments is not defined }; // 用 rest 参数代替 const restLog = (...args) => { console.log(args); }; restLog(1, 2, 3); // [1, 2, 3]6.6 动态上下文场景
如果一段代码需要根据调用方式动态改变this,箭头函数绝对不能用。比如某些设计模式中,函数作为工具库的方法,由不同调用方使用,它需要动态接收调用对象的上下文。
7. 箭头函数 this 的常见误区与面试题解析
关于箭头函数和this,网上讨论非常多,但不少结论是错的。这里我整理几个最常见的误区,顺便帮你看穿几类面试题。
7.1 误区一:箭头函数自动绑定外层 this
这句话不准确。准确的说法是:箭头函数没有自己的this,它访问this时会沿作用域链向上查找。差别在哪里?“自动绑定外层 this”给人一种箭头函数主动做了绑定的感觉,但实际上箭头函数根本没有 this 绑定这一步。它的行为更像是词法查找,和你查一个普通变量一样。
7.2 误区二:箭头函数不能改变 this
更准确的说法是:箭头函数的 this 在定义时已经确定,call、apply、bind无法重新绑定它。但这不代表箭头函数内部的 this 永远不变。如果它外层函数的 this 变化了,箭头函数拿到的 this 也会跟着变化。
const objA = { name: 'A', show: function() { const arrow = () => { console.log(this.name); }; arrow(); } }; const objB = { name: 'B' }; objA.show(); // A // 通过 call 改变 show 的 this,箭头函数拿到的 this 随之改变 objA.show.call(objB); // B这里arrow的定义位置在show内部,所以它查找的是show函数的 this。当show的 this 被改成objB时,箭头函数输出的就是B。所以,说“箭头函数 this 永远固定”是不对的,只能说对箭头函数本身无法显式改变,但它依赖的外层 this 可以被改变。
7.3 误区三:箭头函数完全可以替代普通函数
这是项目里最常见的问题。箭头函数不能用作构造函数,不能当对象方法,没有 arguments。它适合的是回调、嵌套函数、链式调用这类不需要动态 this 的场景,而不是取代普通函数。
7.4 面试题:这段代码输出什么
const obj = { name: '张三', sayHi: () => { console.log(`Hi, ${this.name}`); } }; obj.sayHi();输出Hi, undefined。因为sayHi是箭头函数,没有自己的 this,向上查找全局作用域,全局没有 name 属性。
const obj = { name: '张三', sayHi: function() { const arrow = () => { console.log(`Hi, ${this.name}`); }; arrow(); } }; obj.sayHi();输出Hi, 张三。因为sayHi是普通函数,this 指向 obj,箭头函数定义在sayHi内部,沿作用域向上找到的就是sayHi的 this。
7.5 面试题:延迟输出
const obj = { name: '张三', greet: function() { setTimeout(function() { console.log(this.name); }, 0); } }; obj.greet();输出undefined。setTimeout的回调是普通函数,被独立调用,this 指向全局对象。
如果改成箭头函数:
const obj = { name: '张三', greet: function() { setTimeout(() => { console.log(this.name); }, 0); } }; obj.greet();输出张三。箭头函数沿作用域找到greet的 this,也就是obj。
7.6 面试题:call 能改箭头函数的 this 吗
const arrow = () => { console.log(this.name); }; arrow.call({ name: '张三' });输出undefined。箭头函数没有自己的 this,call传参无效。
8. 实际项目里的最佳实践与工程建议
前面讲了原理和场景,最后落到工程实践。这一节给出具体可执行的建议,帮助团队规范使用箭头函数,减少this相关 bug。
8.1 什么时候优先用箭头函数
- 定时器、事件回调、异步回调内部,需要沿用外层 this。
- 数组高阶函数的回调,代码短且不需要动态 this。
- 函数内部嵌套的辅助函数。
- 返回函数的工厂函数、高阶函数。
- 不需要函数自身 arguments 的短函数。
8.2 什么时候必须用普通函数
- 对象方法,需要 this 访问对象属性。
- 原型方法。
- 构造函数。
- 需要动态 this 的 DOM 事件处理函数。
- 函数内部需要使用 arguments。
8.3 判断标准
一个很简单的判断方法:这个函数是否需要拥有自己的 this?
需要,用普通函数。不需要,用箭头函数。
这里的“需要”不只是“用到 this”,还包括“即使现在不用,未来也可能被调用方动态指定 this”。比如一个事件处理函数,虽然不是你自己调用,但浏览器会往回调里塞一个 this 指向触发元素,这个信息有时是有价值的。
8.4 team lint 规则建议
在 ESLint 中,有一个规则叫prefer-arrow-callback,建议在回调场景使用箭头函数。还有一个规则叫no-this-assignment,会警告类似const that = this这样的写法——当你用了箭头函数后,这种写法基本可以消失。
{ "rules": { "prefer-arrow-callback": "error" } }配置这条规则,会让团队代码中“为了存 this 而定义的变量”明显减少。
8.5 代码审查时重点盯的场景
代码审查时,重点看这几个位置:
- 对象方法是否误用了箭头函数
- 生命周期函数(Vue 的
created、mounted,React 的componentDidMount)内部是否混用了箭头函数和普通函数 - 事件监听回调是普通函数还是箭头函数,是否符合意图
- 是否有人在 class 里把字段直接写成箭头函数(这个在 React 类组件里常见,没问题;但普通类里要确认 this 的预期)
8.6 关于 this 的心智模型
在项目里建议统一一套心智模型,避免每个成员靠记忆“这个场景箭头函数行不行”来写代码。我建议这样理解:
- 普通函数的 this 像一个“调用时注入的外部参数”,由调用方式决定。
- 箭头函数的 this 像一个“定义时捕获的变量”,它从外层作用域继承。
有了这个模型,分析代码时只需要看:
- 箭头函数最近的普通函数作用域在哪里?
- 这个普通函数的 this 是什么?
- 箭头函数拿到的 this 就是什么。
如果箭头函数外面没有普通函数,那 this 就是全局对象(模块作用域下可能是 undefined,取决于模块模式)。
8.7 一段综合练习
最后放一段综合代码,你可以先自己在心里判断输出,再运行验证。
const name = '全局'; const obj = { name: '对象', normalFn: function() { return function() { console.log(this.name); }; }, arrowFn: function() { return () => { console.log(this.name); }; }, arrowMethod: () => { console.log(this.name); } }; obj.normalFn()(); // ? obj.arrowFn()(); // ? obj.arrowMethod(); // ?运行结果:
obj.normalFn()():输出全局。内部返回的是普通函数,独立调用,this 指向全局。obj.arrowFn()():输出对象。箭头函数定义在arrowFn内部,查找的是arrowFn的 this,也就是 obj。obj.arrowMethod():输出全局。arrowMethod本身是箭头函数,没有自己的 this,向上找到全局作用域的name。
在浏览器控制台运行时会发现,第三个输出可能在严格模式下是undefined。这是因为 ES6 模块默认严格模式,this 不会指向全局对象。这个差异也提醒我们:同样的代码,在不同的运行环境下 this 的默认值可能不同。
8.8 生产环境的注意事项
在生产项目中,还有几个实际建议:
不要滥用箭头函数做“性能优化”。箭头函数确实比普通函数少绑定 this,但两者的性能差异在绝大多数业务代码里可以忽略。真正的影响是代码可读性和正确性。
避免在定义对象字面量时让箭头函数阅读起来像是方法。如果团队成员习惯写
const obj = { foo: () => {} },很容易误解为 foo 可以使用 this 访问 obj。建议在代码规范里直接禁止对象字面量使用箭头函数写方法。在 TypeScript 里,箭头函数结合 class 字段也有注意点。类字段箭头函数会在每个实例创建时新建函数,如果实例数量很大,内存开销比原型方法大。这在列表项组件、表格行组件里特别明显。
9. 总结与后续学习方向
这篇文章的核心内容可以归纳成下面这几点。
第一,普通函数的 this 由调用方式决定,有默认绑定、隐式绑定、显式绑定、new 绑定四种规则。这也是 this 经常丢失的原因:函数被独立调用时,this 不再指向你预期的对象。
第二,箭头函数没有自己的 this。它在定义时从外层作用域捕获 this,与调用方式无关。这正是它解决回调函数 this 丢失问题的原因——它直接把外层 this 沿作用域“继承”下来了。
第三,箭头函数不能替代普通函数。对象方法、原型方法、构造函数、需要动态 this 的 DOM 事件等场景,都应该继续使用普通函数。
第四,判断用哪个函数的简单标准是:这个函数是否需要拥有自己的 this。
如果想继续学习,可以往这几个方向深入:
- JavaScript 作用域、闭包和执行上下文的完整机制
- 函数式编程中的高阶函数、柯里化和组合
- Vue 3 或 React Hooks 中 this 设计差异,以及为什么 Hooks 风格更依赖闭包而非 this
- TypeScript 中对函数类型、this 类型的类型声明
建议你写完这篇文章后,找几个项目里的老代码,把其中回调函数里的that、self变量和bind(this)改成箭头函数,再跑一遍测试。这个过程能让你对这些机制的理解落到实处,而不只是记住几条规则。