ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

箭头函数与普通函数this指向:原理、区别与实战避坑指南

箭头函数与普通函数this指向:原理、区别与实战避坑指南 帮一个老项目做维护时同事把排查了半天的 bug 丢给我他在一个回调函数里调用this.loadList页面一进来就报Cannot read properties of undefined代码翻来覆去改了好几遍都没找到原因。我扫了一眼就告诉他把这里的function换成箭头函数试试。他半信半疑地改完功能立刻正常了。他问我是不是靠经验猜的我说这不叫猜这是前端最基础、也最容易让人栽跟头的两组概念——箭头函数和普通函数的 this 指向问题。这个知识点无论在真实开发还是前端面试里都会被反复问到但很多新人始终停留在记住结论的层面一旦换一个场景马上又懵了。这篇内容不打算给你念定义我会把 this 的底层机制拆开揉碎结合几段真实项目里翻车过的代码带你搞明白箭头函数和普通函数到底差在哪以及什么时候该用谁。适合正在学前端基础、准备面试、或者写业务代码时被 this 坑过的人。1. 普通函数的 this调用点在哪儿this 就在哪儿1.1 先搞懂普通函数的 this 到底从哪来很多新人有个误区以为 this 是函数内部一个固定的、天生就存在的变量。实际上普通函数的 this 是在函数被调用的时候才被确定的它指向谁完全取决于这个函数是怎么被调用的。说直白一点同一个函数你用不同方式调用它this 会指向完全不同的东西。JavaScript 里普通函数的 this 大致有四条绑定规则默认绑定没有任何修饰地调用函数比如fn()非严格模式下 this 指向全局对象严格模式下 this 是undefined。隐式绑定函数作为对象的方法被调用比如obj.fn()this 指向这个对象。显式绑定通过call()、apply()、bind()调用函数this 指向你传入的第一个参数。new 绑定用new关键字调用函数时this 指向新创建出来的实例对象。这四条规则有优先级new最高显式绑定其次隐式绑定再次最后才是默认绑定。日常开发里你遇到的大部分 this 问题都发生在第二条和第一条之间的切换上。为了帮你建立直觉我打一个比方把函数当成一个员工this 就是公司发下来的工牌。员工本人能力、经验不变但他被派到哪个项目组干活工牌上就写哪个项目组的名字。你直接把他叫去办公室fn()他工牌上写的是总公司你让某个部门负责人招呼他obj.fn()他工牌上写的就成了这个部门。function showName() { console.log(this.name); } const personA { name: 张三, showName }; const personB { name: 李四, showName }; personA.showName(); // 张三 personB.showName(); // 李四 showName(); // 严格模式下是 undefined非严格模式下是全局的 name同样的一个showName函数因为调用位置不同this 就跟着变。这就是普通函数 this 的本质调用时动态确定。1.2 隐式绑定中最容易翻车的一环方法被拆出来调用数组方法、回调函数、事件绑定里经常出现一个致命操作把对象的方法单独取出来传给别人调用。此时隐式绑定会失效this 直接掉回默认绑定。class Counter { constructor() { this.count 0; } increment() { this.count; console.log(this.count); } } const counter new Counter(); const result [1, 2].forEach(counter.increment);看起来counter.increment是对象方法应该能正常累加对吧但运行之后你会发现要么报Cannot read property count of undefined要么全打印成NaN。原因在于forEach接收了counter.increment这个方法本身并在内部自己用一个普通方式调用了它方法跟 counter 之间的隐式联系就断了this 自然不指向 counter。在 React 类组件流行的那几年这个坑几乎是新人必踩的。你得手动写this.increment this.increment.bind(this)或者用箭头函数作为类属性本质上都是把方法被拆出来调用时 this 丢失这个问题重新堵上。理解到这里你就明白普通函数的脾气了它既是优势也是劣势。优势是灵活谁调用它它就给谁干活劣势是脆弱你稍不注意让它脱离原来的调用方式this 就变脸。2. 箭头函数对 this 做的事它根本没有自己的 this2.1 词法作用域继承this 跟着定义位置走箭头函数的最大不同是它压根没有自己的 this。你访问箭头函数里面的 this 时它会像查找普通变量一样沿着定义时所在的作用域链一层层往上找找到最近的那个普通函数或全局作用域里的 this拿来直接用。你可以把箭头函数的 this 理解为租来的继承来的。它自己不创建 this直接沿用外层的 this。关键在于它看的是定义位置不是调用位置。这是跟普通函数最大的分水岭。const person { name: 王五, getName: () { console.log(this.name); } }; person.getName(); // undefined很多新人会以为person.getName()里 this 指向 person但箭头函数根本不理会这种调用方式。这个箭头函数在定义时外层作用域是全局作用域所以它拿到的 this 就是全局的 this跟你调用它时前面挂的是谁没有任何关系。再来看一个多层嵌套的例子const outer { name: 外层对象, inner: { name: 内层对象, show: function () { const arrow () { console.log(this.name); }; arrow(); } } }; outer.inner.show(); // 内层对象这个箭头函数定义在show这个普通函数内部所以它继承的是show函数的 this。调用outer.inner.show()时show的 this 是inner对象于是箭头函数打印出内层对象。这段代码你只要能把顺序理清就说明已经理解了箭头函数 this 的核心机制。2.2 一个反直觉的事实改调用方式影响不了箭头函数的 this既然箭头函数的 this 由定义位置决定那你用call、apply、bind去修改它都会失败。const obj { name: 黑盒 }; const fn () { console.log(this); }; fn.call(obj); // 不指向 obj fn.apply(obj); // 不指向 obj const boundFn fn.bind(obj); boundFn(); // 同样不指向 obj这里的call不是没执行而是它本应把 this 指向obj但箭头函数压根没有自己的 this它读的是外层作用域的 this你传进来一个对象也没地方挂。这也是面试题里很喜欢考的一个点箭头函数 call/bind组合拳到底有没有用。实际开发中这个特性反而帮我们省了很多事。比如在定时器、事件监听、异步请求这些场景里回调函数希望沿用外层函数的 this你只需要把回调写成箭头函数就不用再像老代码那样写var self this或者.bind(this)了。class Timer { constructor() { this.seconds 0; } start() { setInterval(() { this.seconds; console.log(this.seconds); }, 1000); } } const t new Timer(); t.start(); // 1 2 3 4 ...在这个例子里setInterval回调函数如果写普通函数this 会变成全局对象或者 undefined你必须用 bind 或者变量缓存但改成箭头函数之后它继承start方法的 this也就是 Timer 实例一切顺理成章。这也是箭头函数在各类回调场景里迅速取代var self this写法的最直接原因。3. this 只是第一层差异这两类函数还有五个不同点3.1 arguments箭头函数不认识这个对象普通函数内部可以直接访问arguments它保存着函数调用时传入的所有参数即使你定义的形参没有一一对应也能拿到完整列表。但箭头函数内部没有自己的arguments你强行访问它实际上访问的是外层作用域的arguments。function regular() { console.log(arguments); // [1, 2, 3] } regular(1, 2, 3); const arrow () { console.log(arguments); // 这里是外层作用域的 arguments通常不是函数传入的参数 }; arrow(1, 2, 3);想要在箭头函数中使用类似 arguments 的能力标准做法是使用 rest 参数const arrow (...args) { console.log(args); // [1, 2, 3] }; arrow(1, 2, 3);rest 参数返回的是真正的数组相比 arguments 这种类数组对象还更方便做 map、filter 操作。我见过不少人在箭头函数里直接用了arguments[0]结果拿到外层函数的某个值很隐蔽代码没报错但结果不对排查难度比 undefined 还高。3.2 没有 prototype不能当作构造函数普通函数可以配合new创建实例函数身上也会带上一个prototype属性。箭头函数没有自己的prototype自然也没法作为构造函数来使用。function Person(name) { this.name name; } const p1 new Person(张三); // 正常 const ArrowPerson (name) { this.name name; }; const p2 new ArrowPerson(李四); // 报错ArrowPerson is not a constructor底层逻辑是new过程中要创建一个新对象并把这个新对象作为 this 绑定到函数上同时让新对象的原型指向函数的 prototype。箭头函数没有 this 绑定机制也没有 prototype这两步直接没法走所以 ES6 设计时就明确禁止箭头函数被new。3.3 new.target箭头函数里的 new.target 也是继承来的new.target用来判断函数是否通过 new 调用。普通函数中通过 new 调用时它指向函数本身否则是 undefined。箭头函数没有自己的 new.target同样会从外层作用域继承。这个话题面试官问得不算多但你写工具函数时要注意尤其在判断某个函数能不能被 new 的场景里箭头函数会让你拿到的值跟预期不一致。function RegularFn() { console.log(new.target); // RegularFn 本身 } new RegularFn(); const ArrowFn () { console.log(new.target); // undefined继承自外层而不是 ArrowFn }; new ArrowFn(); // 虽然这里会直接报错3.4 不能用作 Generator 函数Generator 函数需要function*声明而且它内部依赖自己的 this 与执行上下文来处理yield的状态。箭头函数没有自己的 this也不能用yield暂停所以它声明不了 Generator。如果你需要生成器老老实实写function*。function* gen() { yield 1; yield 2; } const iter gen(); console.log(iter.next()); // { value: 1, done: false }3.5 箭头函数对 bind 的冷淡态度前面说过call/apply/bind无法改变箭头函数的 this这里再看一个实际开发中的坑如果你把箭头函数传给一个依赖 this 动态变化的库函数比如一些组件库的内部方法想通过apply把组件实例传进去箭头函数会直接忽略这个实例导致库函数内部无法正常访问该实例。虽然这类情况相对少见但一旦出现排查思路就要从this 被改了切换成箭头函数无视 this 修改。下面用一张表格把差异整理清楚对比维度普通函数箭头函数this 机制调用时动态绑定定义时继承外层 this没有自己的 thisarguments有自己的 arguments没有需要 rest 参数替代作为构造函数可以配合 new不可以prototype有且默认挂一个原型对象没有new.target可以访问继承外层无独立值Generator支持 function*不支持call/apply/bind可以改变 this无法改变 this相同参数名声明非严格模式允许不允许重复4. 看懂了大方向日常开发里到底该用哪个4.1 这些场景优先选箭头函数第一个高频场景就是回调函数。比如setTimeout、setInterval、事件监听器、Vue 组件里的 watch以及 Promise 的 then/catch/finally 回调。这些场景里你通常希望回调继承外层作用域的 this而普通函数恰恰会把 this 搞丢。const loading { show: false, fetchData() { axios.get(/api/list).then(() { this.show true; }); } };这里 then 的回调写成箭头函数this 就能稳稳指向 loading 对象。如果写成 functionthis 就变成了 undefined或全局页面状态永远更新不了。第二个场景是数组遍历方法的回调。map、filter、forEach、reduce这些方法本身会改变回调的 this如果你在回调里有访问外层 this 的需求箭头函数能保证稳定。最常见的例子是 map 内更新组件状态。第三个场景是类字段与对象字面量里的小工具方法。比如 React 类组件中你用箭头函数声明实例字段这样方法作为事件处理函数被调用时this 始终指向组件实例不用 bind。4.2 这些场景必须用普通函数构造函数不用多说箭头函数直接用不了。对象方法如果依赖动态 this就不能用箭头函数。这里要区分对象方法内部是否要用到 this。如果方法不需要 this箭头函数也没问题但如果需要方法被调用时的对象上下文用普通函数更合理。const user { name: 张三, greet() { return 你好我是${this.name}; } };需要 arguments 对象的函数也建议用普通函数虽然 rest 参数能替代但如果你在写工具函数想保持传统风格普通函数更顺手。Generator 函数必须用 function*这点没有讨论空间。还有一类场景容易被忽略动态 this 依赖的库函数。比如一些事件处理库、装饰器实现、框架内部会在调用你传入的函数时用 call 来注入上下文这时你必须给对方一个普通函数让它有机会改变 this。// 某些 Hook 库内部实现大致是 function useDynamic(callback) { const ctx { count: 100 }; callback.call(ctx); } // 如果你的 callback 是箭头函数ctx 根本不会被用到4.3 Vue 和 React 项目里 this 的表现天差地别Vue 2 的 methods、computed、watch 中的函数内部都要求 this 指向组件实例。如果你在 Vue 2 的 methods 里把方法定义成箭头函数this 就不会指向组件实例访问this.someData会直接报错。所以 Vue 2 项目里methods 中强烈建议写普通函数只在回调嵌套里用箭头函数。// Vue 2 组件中 methods: { init() { // 这里必须是普通函数this 才是组件实例 getData().then(() { // 这里写成箭头函数才能继续使用组件实例的 this this.loading false; }); } }Vue 3 的 setup 中通常不依赖 this函数和变量大多通过返回值暴露给模板所以箭头函数在里面没有明显的 this 问题你可以根据自己的风格自由选择。React 类组件中事件处理函数如果不想写 bind推荐用箭头函数类属性函数组件里基本没有 this 的概念箭头函数用起来很自然但也要注意 Hook 回调中可能存在的闭包问题这跟 this 关系不大但同样影响代码正确性。5. 翻车案例实测我把完整排查链路写给你看5.1 setTimeout 回调里 this 不翼而飞这是新人最容易遇到的场景我拿一段真实简化过的代码来演示const counter { count: 0, start() { setInterval(function () { this.count; console.log(this.count); }, 1000); } }; counter.start();你预期每秒输出 1、2、3结果每次输出的都是NaN。原因是 setInterval 内部回调被全局调用function的 this 指向全局对象全局对象上没有 count所以this.count相当于undefined结果就是 NaN。完整排查思路打开浏览器控制台在setInterval回调第一行加console.log(this)发现 this 是 window不是 counter。确认回调是用普通函数写的触发调用时动态绑定。改成箭头函数让 this 继承 start 方法的 this。再打印一次确认 this 是 counter问题解决。如果你在严格模式下这个回调里的 this 会是 undefined报错内容会变成TypeError: Cannot read properties of undefined (reading count)会更吓人。修复后的代码const counter { count: 0, start() { setInterval(() { this.count; console.log(this.count); }, 1000); } }; counter.start();5.2 事件监听器里 this 被浏览器强行替换用原生 JavaScript 给按钮绑定点击事件时如果回调写的是普通函数在回调内部 this 会指向触发事件的元素而不是你原本想要的对象。const handler { message: Hello, show: function () { alert(this.message); } }; const btn document.getElementById(btn); btn.addEventListener(click, handler.show);浏览器会把事件监听回调里的 this 指向按钮 DOM 元素所以this.message输出 undefined页面不会弹出 Hello。这里和 Vue/React 的框架内部绑定不一样原生事件监听器就有这个规定。修复方式有两种一种是用箭头函数作为包装btn.addEventListener(click, () handler.show());另一种是用 bindbtn.addEventListener(click, handler.show.bind(handler));这两种都行但从可读性来讲第一种更直白你在回调里手动去调用 handler 的方法this 自然就是 handler。第二种虽然能用但 bind 生成的新函数除非你单独保存否则后续removeEventListener时不好移除容易导致事件重复绑定。5.3 把对象方法传出去当回调this 直接丢失这个案例比 setTimeout 还要隐蔽因为你看到代码时多半是在另一层文件里传参根本不会第一时间想到 this 出了问题。比如这样一个场景你封装了一个工具函数它接收一个回调并在内部异步执行它。const user { name: 赵六, getName: function () { return this.name; } }; // 某个工具函数 function callIt(callback) { setTimeout(callback, 100); } callIt(user.getName); // undefined这里user.getName被当作参数传给 callIt函数和对象之间的隐式绑定关系已经断了等到 setTimeout 在全局作用域执行回调时this 当然不会是 user。如果你把 callIt 内部改成 setTimeout 外再callback.call(user)这种形式就不是用户侧能控制的了。排查这样问题的经验是看回调从哪里来。只要函数是从对象里取出来的、又作为参数传递就高度怀疑 this 会丢。解决办法依然是箭头函数包装一层或者干脆在对象里就把方法定义成箭头函数。但这里要小心对象里定义箭头函数时this 不指向这个对象而是继承外层作用域。所以更推荐在外面包装callIt(() user.getName()); // 返回 赵六5.4 Vue 项目里 axios 回调中 this 指向变了Vue 2 项目里axios 请求成功后的回调如果你写成普通函数this 会丢失组件实例导致this.loading false执行时报错或悄悄失效。export default { data() { return { loading: false }; }, methods: { fetchList() { axios.get(/api/list).then(function (res) { this.loading false; // 这里 this 不是组件实例 }); } } };我在真实项目里见过很多次这类问题的表现是进入页面后 loading 一直转圈因为请求完成后 loading 状态没有被更新。新人往往去找 loading 的实现问题或者去查 promise 的 then 为什么没执行但真正的原因就是 this 指向变了。排查方法还是老套路在 then 回调里先console.log(this)看它到底是谁。如果是 undefined 或 window再回头看一下回调是普通函数还是箭头函数。改成箭头函数之后this 继承的是fetchList方法里的组件实例问题立刻解决。5.5 以后你自己排查 this 问题用这几步就行根据我踩过的一堆坑总结一套排查this问题的流程你可以直接照着用定位 this 出现的函数是箭头函数还是普通函数。如果是箭头函数this 从外层作用域找跳到第 3 步如果是普通函数进入第 2 步。看这个普通函数怎么被调用是直接调用、对象方法调用、call/apply/bind 调用还是 new 调用。判断对应的 this 应该是什么。如果 this 不是你预期的对象检查外层作用域最近的那层普通函数以及它自己的 this 规则。箭头函数的 this 最终一定追溯到某个普通函数或全局作用域。在代码里加console.log(this)把实际打印出来的值和预期做对比不要靠猜。决定修复方式如果要使用外层 this就把回调改成箭头函数如果要使用动态 this就保留普通函数并把箭头函数改成普通函数或调整调用方式。6. 个人经验与一点补充聊到这儿箭头函数和普通函数之间那些该知道的东西基本都过了一遍。最后分享一个我很受用的小习惯接手别人代码时先全局搜一遍箭头函数和普通函数混用的文件尤其是回调函数里的 this。很多看起来诡异的状态更新失败、异步数据渲染不出来根因都在这一层。再补充一个实用技巧在项目里启用 ESLint 的prefer-arrow-callback和arrow-body-style规则可以帮你自动识别哪些回调适合改成箭头函数哪些地方坚持用普通函数。但注意这不是银弹ESLint 静态分析判断不了 this 的真实指向最终还是要靠你在脑子里跑一遍调用时绑定还是定义时绑定的逻辑。如果你刚从前端面试完或者正在准备前端面试这道题最容易被追问的其实不是箭头函数有哪些特性而是箭头函数为什么不能当构造函数箭头函数能用 call 改变 this 吗。这两个问题我在文中都给了底层解释希望你能带着理解去回答而不是背答案。实际开发里我不喜欢一刀切地说尽量用箭头函数或尽量用普通函数。策略应该是动态 this 场景用普通函数静态 this 场景用箭头函数。分清场景比死记规则重要得多。遇到一个函数先问它需不需要自己的 this、要不要被 new、会不会被库函数动态修改上下文答案自然就出来了。
返回列表