免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

深入理解JavaScript this绑定机制与箭头函数词法作用域

深入理解JavaScript this绑定机制与箭头函数词法作用域 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)指向传入的ctxnew 绑定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而不是运行时 this3.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 运行环境要求箭头函数是 ES6ECMAScript 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指向全局对象浏览器中的windowNode.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); // 仍然输出外层 thistarget 被忽略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等数组方法常常需要在外层对象方法的上下文里访问thisconst 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值得多说一句。数组方法的回调如果使用普通函数可以通过第二个参数显式传入 thisconst 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 在严格模式下是undefinedresult.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导致接口调用异常箭头函数本身没有 thiscall 传入上下文被忽略检查函数是否用箭头函数定义需要动态 this 的函数改用普通函数class 中方法用箭头函数每个实例占用额外内存类字段箭头函数会在每个实例创建新函数打印counter.increase counter.autoIncrease观察只在必须绑定的场景使用普通场景用类方法 构造器 bind事件监听回调中 this 不是预期对象普通函数作为事件监听器 this 指向 DOM 元素在回调内打印 this用箭头函数保留外层 this但要确认不依赖 DOM thisVue 选项 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 性能与内存箭头函数的运行时性能与普通函数基本没有差别不必担心性能开销。唯一需要注意的是类字段箭头函数在实例上的使用每个实例创建独立的函数对象会占用更多内存也会导致原型链上无法覆盖方法。如果类实例数量很大建议还是使用普通方法加构造器 bindclass 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 时可以直接翻出来对照排查。
返回列表