尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

深入理解JavaScript箭头函数this绑定规则与工程实践指南

深入理解JavaScript箭头函数this绑定规则与工程实践指南 很多前端同学在项目里写了半年箭头函数一到排查线上 bug 时还是会卡在this上。最常见的剧情是这样的在 Vue 或小程序里methods中明明能访问到this一进setTimeout或者请求回调this突然就变成undefined或者 window 了。老手会顺手写一行var that this再往上是.bind(this)再后来有人告诉你“用箭头函数就行了”。你照着改了发现确实能用了但心里始终有一个疑问箭头函数的this到底是怎么绑定的为什么有时候它好用有时候在对象方法里又不能用这篇文章就从this的绑定规则讲起把箭头函数为什么能解决“this 丢失”讲透再给出完整代码示例、典型应用场景、常见的坑和工程规范。读完你能收获三件事第一看懂this的四种绑定规则第二彻底理解箭头函数“词法 this”的本质第三形成一套“什么时候该用箭头函数、什么时候不该用”的判断标准。1. 这篇文章真正要解决的问题先问一个更直接的问题为什么你写普通函数时this会“丢”很多人以为是“作用域”问题于是想当然认为箭头函数把this绑定到了“上一层作用域”。这句话只说对了一半。普通函数的this不是静态的它在调用时根据调用方式动态决定而箭头函数的this是静态的它在函数定义时从外层词法作用域读取。两者差异的根源就在这里。这个问题在真实项目里影响面很大在组件的methods中调用异步接口回调函数里访问不到this。在事件监听器addEventListener里注册回调拿不到外层组件的this。在数组的map、filter回调里想读取外部对象的属性结果报this is undefined。在类的方法里嵌套setTimeout回调函数里的this指向了全局对象。在 React 类组件中忘记绑定事件处理函数点击时直接报错。在小程序页面中调用wx.request成功后回调里访问不到this.setData。这些问题的共通点是你把一个普通函数作为“回调”传给了别的 API调用时这个函数已经脱离了原来的对象this自然就变了。箭头函数并不是凭空创造了新的this规则它只是通过“词法捕获”的方式让this不再随调用方式变化。理解这一点之后箭头函数才能从“玄学”变成“依据”。1.1 一个容易混淆的误区很多人以为箭头函数“没有 this”或者“箭头函数的 this 指向外层函数的 this”。这两种说法都不够准确。更精确的说法是箭头函数本身没有自己的this绑定所以它不会在调用时创建this。当你在箭头函数内部访问this时JavaScript 引擎会沿着作用域链向上查找找到最近的普通函数作用域中的this。打个比方普通函数像是一个“随身带着自己的姓名牌”的员工入职时是什么名字工作时就用什么名字领导可以指定他叫“张三”或“李四”箭头函数则像是“没有姓名牌的实习生”他填表时永远填“看部门负责人是谁”。这个“部门负责人”就是它定义位置所在的外部函数。1.2 文章范围说明本文默认使用现代 ECMAScript 语法代码示例基于浏览器环境和 Node.js 环境均可运行。涉及class、setTimeout、Promise、addEventListener等 API 时会给出明确的运行环境说明。版本细节不是重点重点是把建立this绑定关系的思路讲清楚。2. 先看普通函数的 this四种绑定规则想理解箭头函数必须先理解普通函数的this。很多文章直接讲箭头函数默认读者已经掌握了普通函数规则结果读者更晕了。这里把普通函数的规则压缩成四类。2.1 默认绑定独立函数调用时this指向全局对象在严格模式下指向undefined。// 非严格模式 function show() { console.log(this); // 浏览器中为 window } show();use strict; function show() { console.log(this); // undefined } show();这是最简单的规则也是“this 丢失”最常见的真相。把函数从对象里抽出来赋值给一个变量再调用这个变量就触发了默认绑定。2.2 隐式绑定当函数作为对象的方法调用时this指向调用该方法的对象。const user { name: 张三, greet() { console.log(this.name); } }; user.greet(); // 张三隐式绑定成立的前提是“调用时”明确了对象比如user.greet()。如果拆开来调用const fn user.greet; fn(); // this 丢失非严格模式下输出 undefined严格模式报错这就是方法调用和函数独立调用的区别。2.3 显式绑定通过call、apply、bind可以手动指定this的指向。function sayName(age, city) { console.log(this.name, age, city); } const person { name: 李四 }; sayName.call(person, 18, 北京); // 李四 18 北京 sayName.apply(person, [18, 北京]); // 李四 18 北京 const bindFn sayName.bind(person, 18); bindFn(上海); // 李四 18 上海call和apply的区别是参数传递方式不同bind会返回一个绑定了this的新函数。2.4 new 绑定当一个函数用new调用时this指向新创建的对象。function Person(name) { this.name name; } const p new Person(王五); console.log(p.name); // 王五new会创建一个对象、把函数内的this绑定到这个对象、执行函数体、返回这个对象。2.5 四条规则对比绑定方式调用形态this 指向默认绑定fn()非严格模式下是全局对象严格模式下是 undefined隐式绑定obj.fn()调用方法的对象 obj显式绑定fn.call(obj)/fn.apply(obj)/fn.bind(obj)指定的对象 objnew 绑定new Fn()新创建的对象实例普通函数的this在调用时确认所以才有“动态绑定”这一说法。它虽然灵活但是在回调场景下很容易出现“丢失”问题。3. 箭头函数的 this本质是词法绑定箭头函数是 ES6 引入的语法它的本名其实不是“箭头函数”这一个特性而是“不绑定自己的 this、arguments、super 和 new.target”的函数。先说结论箭头函数没有自己的this但它可以通过作用域链访问到外层函数的this。这个外层函数在哪里定义箭头函数的this就指向哪里而不是看它在哪里被调用。3.1 作用域链上的 this 才是关键看下面这个例子const obj { name: 示例对象, normalFn: function () { console.log(普通函数, this.name); }, arrowFn: () { console.log(箭头函数, this.name); } }; obj.normalFn(); // 普通函数示例对象 obj.arrowFn(); // 箭头函数undefined为什么obj.arrowFn()输出undefined因为箭头函数定义在对象字面量obj内部但对象字面量不是“函数作用域”它不会创建一个可供this查询的作用域。箭头函数向外层找找到的是全局作用域所以this是全局对象此时全局对象上并没有name属性因此输出undefined。这个例子说明了一个高频误区不是“箭头函数定义在对象里this就指向对象”而是“箭头函数的外层必须有一个函数作用域this才可能拿到值”。3.2 箭头函数不是“最外层 this”而是“定义时的 this”再看更常见的场景const obj { count: 0, start: function () { const inner () { console.log(this); }; inner(); } }; obj.start(); // 输出 obj这里的inner是箭头函数它定义在start的函数作用域内所以它捕获的是start函数调用时的this。start是通过obj.start()隐式绑定调用的因此this是obj。箭头函数输出obj就是词法捕获的结果。3.3 箭头函数可以用 call/apply/bind 改变 this 吗不能。const fn () { console.log(this.name); }; const target { name: 改不了 }; fn.call(target); // 输出 undefinedthis 仍然是定义时的外层 this箭头函数内部使用this时引擎不会执行绑定逻辑因此call、apply、bind对箭头函数的this无效。这也是容易踩坑的地方有些同学看到bind能解决普通函数的问题就去bind一个箭头函数结果发现并没有效果。3.4 箭头函数没有的其它东西没有arguments对象但可以使用 rest 参数收集剩余参数。不能作为构造函数不能和new一起使用否则抛出TypeError。没有prototype属性。不能作为生成器函数不能使用yield。这些限制也决定了箭头函数的适用边界。比如你不能写一个“箭头函数的类”不能指望它拥有prototype来给实例提供方法也不能依赖arguments.callee这类历史用法。4. 箭头函数的完整语法与返回值箭头函数的语法很短但越短的语法越容易在细节处出错。下面按常见的写法分类说明。4.1 不同参数形式的写法// 无参数 const f1 () { console.log(无参数); }; // 一个参数可以省略括号 const f2 x x * 2; // 多个参数必须加括号 const f3 (x, y) x y; // rest 参数 const f4 (...args) args.length; // 默认参数 const f5 (x 1, y 2) x y;一个参数的场景最容易被忽略比如arr.map(x x * 2)里的x可以不加括号。但如果使用多个参数忘记加括号会直接语法报错。4.2 函数体与返回值// 简洁体直接返回表达式 const add (a, b) a b; // 块体需要 return const add2 (a, b) { const result a b; return result; };简洁体和块体的区别是简洁体只能写一个表达式且这个表达式的值会被自动返回块体可以有任意多行但没有return时返回undefined。4.3 返回对象字面量的坑如果箭头函数简洁体要返回一个对象字面量必须把对象用括号包起来// 错误写法花括号被当成函数体 const getObj () { name: 张三 }; console.log(getObj()); // undefined // 正确写法用圆括号包裹对象 const getObj2 () ({ name: 张三 }); console.log(getObj2()); // { name: 张三 }() {}会被解释为箭头函数的空函数体而不是一个对象。遇到返回对象的需求记得加上一层括号。4.4 箭头函数与解构参数在数组或对象方法中经常配合解构简化代码const users [ { name: 赵六, age: 20 }, { name: 钱七, age: 30 } ]; const names users.map(({ name }) name); console.log(names); // [赵六, 钱七]这里参数列表({ name })是对对象的解构写法上和普通函数参数解构一致。5. 箭头函数典型应用场景与代码实现理解了原理再看应用场景就顺了。箭头函数真正解决的是“回调函数中 this 丢失”的问题下面给出四个最典型的场景。5.1 用箭头函数修复定时器中的 this 丢失问题代码const timer { count: 0, start: function () { setInterval(function () { this.count; console.log(this.count); }, 1000); } }; timer.start(); // 运行结果每秒输出 NaN 或报错原因setInterval的回调是独立调用普通函数里的this默认绑定到全局对象非严格模式或undefined严格模式。修复方式一缓存 thisconst timer { count: 0, start: function () { const self this; setInterval(function () { self.count; console.log(self.count); }, 1000); } };这种方式能解决问题但多了一个self变量代码不够简洁。修复方式二箭头函数const timer { count: 0, start: function () { setInterval(() { this.count; console.log(this.count); }, 1000); } }; timer.start(); // 输出1 2 3 4 ...箭头函数捕获了start函数作用域内的this而start是作为timer的方法调用的所以this指向timer。这是箭头函数最常见的收益之一。5.2 在数组中用箭头函数读取外层 thisconst calculator { factor: 10, multiply: function (numbers) { return numbers.map(function (num) { return num * this.factor; // 这里会报错 }); } }; console.log(calculator.multiply([1, 2, 3]));普通函数在map回调中作为独立函数调用this不是calculator。改为箭头函数const calculator { factor: 10, multiply: function (numbers) { return numbers.map(num num * this.factor); } }; console.log(calculator.multiply([1, 2, 3])); // [10, 20, 30]回调里的箭头函数定义在multiply函数作用域内this随multiply走而multiply由calculator调用所以this.factor能正确取到。5.3 Promise 链与异步流程中的 this现代前端开发中fetch、axios、wx.request等异步接口都是基于 Promise 的。Promise 回调是典型的延迟执行场景普通函数在回调里很容易丢失this。const api { baseUrl: https://api.example.com, request: function () { fetch(this.baseUrl /list) .then(function (res) { return res.json(); }) .then(function (data) { // 这里的 this 不是 api会报错 this.handleData(data); }); }, handleData: function (data) { console.log(处理数据, data); } };使用箭头函数改写const api { baseUrl: https://api.example.com, request: function () { fetch(this.baseUrl /list) .then(res res.json()) .then(data { this.handleData(data); // this 指向 api }); }, handleData: function (data) { console.log(处理数据, data); } };箭头函数在then回调中依然持有外层request方法的this因此可以继续调用this.handleData。需要注意的是上面的例子为了演示简洁把业务逻辑都写在了对象方法内部。真实项目中更推荐把 API 层抽成独立模块再通过类或组合方式组织这样this的来源会更清晰。5.4 类中定义箭头函数属性ES6 的类语法默认方法在原型上如果直接把方法传给事件监听器this可能会丢失。类属性上使用箭头函数是一种常见解法。class Counter { constructor() { this.count 0; } increment () { this.count; console.log(this.count); }; } const counter new Counter(); document.getElementById(btn).addEventListener(click, counter.increment);每次点击按钮increment里的this都指向counter实例因为箭头函数定义在类字段初始化阶段捕获了实例作用域下的this。但这里有一个工程取舍类的普通方法属于原型所有实例共享一份类的箭头函数属性会在每个实例创建时都复制一份函数。如果实例数量很大内存占用会比普通方法高。在业务组件中影响通常不大但在高频创建的场景中值得权衡。5.5 事件监听器中的对比addEventListener回调里普通函数的this指向当前绑定元素而箭头函数的this指向外层定义环境。const component { name: 组件A, bindButton: function (buttonId) { const button document.getElementById(buttonId); button.addEventListener(click, function () { console.log(普通函数 this, this); // 指向 button 元素 console.log(name, this.name); // undefined }); button.addEventListener(click, () { console.log(箭头函数 this, this); // 指向 component console.log(name, this.name); // 组件A }); } };这里没有谁绝对优于谁而是取决于你要访问什么需要访问event.currentTarget或按钮自身的属性普通函数更直接。需要访问外层组件的方法和状态箭头函数更自然。如果既要访问元素又要访问外层组件可以这样写button.addEventListener(click, (event) { this.handle(event.currentTarget); })。6. 不该用箭头函数的场景箭头函数不是万能药。有些场景使用箭头函数会让代码更难维护或者直接产生错误。以下五类情况要特别注意。6.1 对象方法中使用箭头函数前面第 3 节已经演示过对象字面量里的箭头函数this不指向对象本身。如果强行使用this会指向外层作用域导致状态读写失效。const counter { count: 0, increment: () { this.count; // this 不是 counter } }; counter.increment(); console.log(counter.count); // 0并没有增加除非你能明确知道外层作用域就是你想访问的对象否则对象方法应该使用普通函数。6.2 原型方法中使用箭头函数类的普通方法天然在原型上如果写成箭头函数该方法不会出现在原型上而是成为实例自身的属性。class Animal { constructor(name) { this.name name; } speak () { console.log(${this.name} 叫了一声); }; }简单看功能没问题但如果你依赖原型链做继承或方法覆盖箭头函数会带来额外复杂度。更清晰的工程建议是默认使用普通方法当需要把方法当作回调直接传递时再考虑使用箭头函数字段或在构造器中手动绑定。6.3 需要动态 this 的事件监听事件处理中如果你想通过this获取触发事件的元素使用箭头函数就得不偿失。const buttons document.querySelectorAll(.btn); buttons.forEach(btn { btn.addEventListener(click, function () { console.log(this); // 当前点击的按钮方便直接读取>const Person (name) { this.name name; }; const p new Person(张三); // TypeError: Person is not a constructor如果你的代码里有工厂函数、构造函数或需要new调用的函数必须使用普通函数。6.5 需要 arguments 对象时普通函数内部有arguments箭头函数没有。虽然大多数场景推荐使用 rest 参数替代但如果你依赖arguments.length等能力就要注意箭头函数的表现。function normalFn() { console.log(arguments.length); } const arrowFn () { console.log(arguments); // 这里引用的是外层作用域的 arguments不是本函数的 };箭头函数里的arguments会向外层作用域查找如果外层也没有就会报错。建议统一使用 rest 参数const arrowFn (...args) { console.log(args.length); };6.6 深层嵌套时别让 this 变得“太隐晦”箭头函数可以让this沿着作用域链一直向上找。如果嵌套层级很深阅读代码的人很难快速判断this到底来自哪一层排错成本反而上升。遇到这种情况不如把this显式传给内部函数或者提升到最上层统一处理。7. 常见问题与排查思路下面的表格汇总了实际开发中高频出现的问题。记住一个总原则遇到this异常先判断这是普通函数还是箭头函数再看它的定义位置和调用方式。问题现象可能原因排查方式解决方案回调函数里this是 undefined普通函数独立调用触发默认绑定检查该函数是普通函数还是箭头函数使用箭头函数、bind或缓存外部 this箭头函数里this拿不到对象属性箭头函数定义处的外层没有 this检查定义作用域是否是某个函数内部把逻辑改到普通函数方法中或确认外层 this 来源对象方法用箭头函数后调用不了属性箭头函数 this 不是对象在对象字面量中打印 this改用普通函数方法箭头函数 bind 不生效箭头函数没有自己的 this直接实验fn.call(obj)改用普通函数或调整捕获外层 this 的位置点击事件拿不到按钮元素箭头函数 this 指向外层打印 this 和 event使用普通函数或使用event.currentTarget类中箭头函数属性内存较高每个实例都创建函数用Object.hasOwn检查实例属性实例少时可用否则用普通方法加 bind返回对象时报 undefined简洁体把对象花括号当成函数体检查返回值是否被圆括号包裹改成() ({ ... })使用 new 调用箭头函数报错箭头函数不能作为构造函数查看栈报错信息改用普通函数或 class箭头函数里没有 arguments箭头函数不创建 arguments打印 arguments 看是否来自外层改用 rest 参数(...args)下面针对几个高频场景补充排查示例。7.1 setTimeout 里的 this 是 undefined代码const app { init: function () { setTimeout(function () { console.log(this); }, 100); } };输出严格模式下是undefined非严格模式下是全局对象。排查顺序确认外层init是普通函数还是箭头函数。确认setTimeout回调是普通函数还是箭头函数。如果是普通函数那么this由运行时决定而不是由定义位置决定。修复方式把回调改成箭头函数或在setTimeout外缓存const self this。7.2 在类方法中给数组回调用了 this代码class Store { constructor() { this.items [1, 2, 3]; } getDoubled() { return this.items.map(function (item) { return item * this.factor; // this 不是 Store 实例 }); } }这里更推荐箭头函数getDoubled() { return this.items.map(item item * this.factor); }7.3 如何在浏览器中快速验证 this 指向最简单的验证方式是在代码里临时输出const obj { name: 验证, testFn: function () { console.log(普通函数 this.name:, this.name); const arrow () { console.log(箭头函数 this.name:, this.name); }; arrow(); } }; obj.testFn();控制台输出普通函数 this.name: 验证 箭头函数 this.name: 验证如果你把testFn改成箭头函数两个输出都会变成undefined。8. 工程最佳实践与代码规范箭头函数本身不复杂复杂的是“哪种地方该用”。这一节把工程里的常用标准整理成可执行的原则。8.1 一个判断标准this 应该是“定义时的”还是“调用时的”希望this跟随外层作用域不随调用方式变化用箭头函数。希望this由调用者决定例如对象方法、事件回调、构造函数用普通函数。这条标准几乎覆盖了绝大多数选型。再往下细化可以这样分类代码位置推荐方式原因对象方法普通函数方法调用时 this 指向对象本身类的方法普通函数必要时用箭头函数字段正常方法在原型上共享回调传递时用箭头函数字段避免丢失函数参数回调箭头函数优先简洁且能自然继承外层 thissetTimeout / setInterval 回调箭头函数优先避免 this 丢失Promise / fetch 回调箭头函数优先方便继续使用外层 this 和更简洁的返回值事件监听回调看需求需要用 element 对象时用普通函数需要访问外层组件时用箭头函数构造函数普通函数箭头函数不能 new需要 arguments普通函数或 rest 参数箭头函数没有 arguments8.2 ESLint 规则建议工程化项目通常使用 ESLint 统一规范。与箭头函数相关的规则可以这样配置{ rules: { prefer-arrow-callback: warn, arrow-body-style: [error, as-needed], arrow-parens: [error, as-needed], no-this-before-super: error, class-methods-use-this: off } }说明prefer-arrow-callback在回调函数中优先使用箭头函数。arrow-body-style强调函数体尽量使用简洁体。arrow-parens根据团队习惯控制参数括号as-needed表示单参数可不加括号。class-methods-use-this类方法中不使用 this 时是否警告建议关闭因为很多渲染函数本身不访问 this。如果项目是 Vue 3 组合式 API 或 React 函数组件大多数业务代码都在函数作用域内箭头函数和普通函数混用时要优先考虑可读性。8.3 代码审查时需要重点看的场景在 Code Review 中我会把“this 指向”列为重点检查项。看到以下代码时要特别谨慎对象方法中使用了箭头函数。类字段箭头函数数量过多。在一个普通函数内部又定义了一个箭头函数而箭头函数里读取了this。回调函数嵌套超过三层。bind、call、apply出现在箭头函数上。每遇到一处都让写代码的人解释一下this的来源。解释不清楚就是潜在的 bug。8.4 尽量让 this 的来源在函数定义处清晰可见一个实用的技巧是箭头函数本身不会产生新的this层级所以如果你发现一个函数里面的this很难判断来自哪里问题往往出在外层的普通函数。在复杂组件中可以刻意把需要绑定this的函数放在同一层避免一层一层向外找。例如class Component { constructor() { this.loadData this.loadData.bind(this); } loadData() { // 普通方法构造函数里已经绑定了 this } init() { // 回调里直接传 this.loadData fetch(/api/data).then(this.loadData); } }这种方式在类组件时代很常见。在函数组件或组合式 API 时代你会更多依赖闭包而不是this但this的理解仍然是阅读源码的刚需。9. 总结与后续学习方向如果把这篇文章压缩成一句话那就是普通函数的this由调用方式决定箭头函数的this由定义位置决定。箭头函数真正解决的是“回调函数中的 this 丢失”问题让你不再需要var self this或者频繁.bind(this)。它同时带来了简洁的语法、词法this、没有arguments、不能new等限制这些限制一起勾勒出它的使用边界。建议你按下面三个步骤继续强化第一步亲手用原生 JavaScript 写一个timer对象分别用普通函数、self缓存、bind和箭头函数实现计数功能观察控制台输出差异。第二步在一个 Vue 或 React 项目中练习在methods或类组件方法里调用fetch请求然后把回调从普通函数改成箭头函数对比this的变化。第三步阅读一下项目里已经存在的箭头函数代码尝试在注释中标出每个箭头函数this的来源。只有你能准确说出来源才算真正吃透了箭头函数。this绑定是 JavaScript 面试中高频考察的知识点也是代码维护中高频出错的地方。如果你能把这套规则形成自己的判断习惯再回头看业务代码很多历史遗留的“奇怪写法”就会变得非常清晰。
返回列表