十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

JavaScript箭头函数与this绑定机制:从动态调用到词法捕获

JavaScript箭头函数与this绑定机制:从动态调用到词法捕获 箭头函数在 JavaScript 里的普及程度有多高几乎每一段现代代码里都能看到它的身影。但你可能也见过这样的场景有人把回调函数改成了箭头函数发现 this 不丢了很开心但下一次在对象方法里用箭头函数结果 this 又指到了全局对象浏览器控制台直接给你一个 undefined。同样的箭头函数为什么一会有效一会无效这就是很多开发者对 this 一直似懂非懂的根源箭头函数不是把 this 变简单了而是换了一套绑定机制。真正决定 this 朝哪指的不是“你写没写箭头函数”而是“这个函数在定义时捕获了哪个 this”。这篇文章要解决的问题不是背规则而是建立一套判断逻辑。你不需要记住所有边角情况只需要回答一个问题这个函数需要的是调用它的对象还是定义它的外层上下文。回答完这个问题this 的绑定基本就不会再成为拦路虎。1. 你以为箭头函数是“不会丢 this 的普通函数”其实不是很多人的学习路径是这样的先遇到回调函数里 this 丢失然后听说箭头函数能解决这个问题试了一下确实有效。于是大脑里就形成了一个结论箭头函数不会丢 this。这个结论很危险因为它只描述了现象没解释机制。等你换一个场景在对象方法里用箭头函数结果 this 指向完全不对你又会觉得箭头函数是玄学。1.1 一个最常见的回调场景为什么 setTimeout 里的 this 不是你要的先看一个典型问题。假设有一个对象希望延迟一秒后打印自己的名字const user { name: tom, greet: function () { setTimeout(function () { console.log(this.name); }, 1000); } }; user.greet(); // 在浏览器控制台里通常是 undefined或者报错这里普通的 function 回调在执行时相当于被 setTimeout 内部的机制当成一个普通函数调用了。在严格模式下普通函数调用 this 是 undefined所以this.name访问不到。改成箭头函数const user { name: tom, greet: function () { setTimeout(() { console.log(this.name); // tom }, 1000); } }; user.greet();成功了。于是很多人开始得出结论箭头函数能保 this。但问题是为什么能保1.2 箭头函数不是“不会丢 this”而是它没有自己的 this准确的说法是箭头函数本身没有 this 绑定。它内部的 this 不是自己的而是来自定义箭头函数时所在的词法作用域。上面例子里的箭头函数定义在greet函数内部。greet是一个普通函数调用方式是user.greet()所以greet内部的 this 指向 user 对象。箭头函数把这个 this 捕获了于是箭头函数内部的 this 也指向 user 对象。箭头函数并没有具备什么“防丢 this”的魔力。它只是沿用了外层函数作用域的 this。理解这一点是吃透箭头函数的分水岭箭头函数里的 this 不是由“怎么调用”决定的而是由“在哪里定义”决定的。如果定义箭头函数的地方没有外层函数比如在模块最顶层那么它捕获的就是全局 this浏览器里是 window严格模式下模块顶层也是 undefined 或者全局对象取决于环境。2. 普通函数丢失 this 的本质this 不是函数自带的属性要理解箭头函数还得回到普通函数。很多困惑其实不是箭头函数带来的而是普通函数的 this 机制本身。普通函数里的 this 是在调用的时候确定的不是定义的时候确定的。这意味着同一个函数用不同方式调用this 指向完全不一样。2.1 调用方式表this 由“谁调用了它”决定把普通函数的 this 规则用一张表表示会清楚很多调用方式函数内部的 this直接调用fn()严格模式下是undefined非严格模式下是全局对象对象方法调用obj.fn()obj构造函数new Fn()新创建出来的实例call/apply/bind调用手动指定的第一个参数回调函数setTimeout(fn)取决于宿主如何调用通常变成全局对象或undefined事件回调addEventListener(fn)绑定事件的元素注意表里最后一行回调函数被传入某个 API 后它内部的 this 通常和你的预期不一致。因为你把函数传给了别人别人去调用它的时候是按它自己的规则来调用的它不会帮你把刚才的 this 保存下来。2.2 回调函数的问题本质函数作为值传递时调用方式丢失了为什么setTimeout回调里的 this 会丢因为回调函数本质上是作为一个值传给了 setTimeoutsetTimeout 在内部执行这个函数时不管你在外部是怎么调用它的它内部只是简单地执行// 理解成这样的调用方式 fn();此时函数内部 this 走的是“直接调用”规则。如果你写的函数体是function () { console.log(this.name); }这里的 this 自然就不是你的 user 对象了。数组方法也是同理。比如const numbers [1, 2, 3]; const obj { factor: 10, multiply: function () { return numbers.map(function (n) { return n * this.factor; }); } }; obj.multiply(); // 报错因为 map 回调里的 this 不是 objmap 的方法签名里虽然不接受 this 参数但执行回调时同样是直接调用所以回调里的 this 就丢了。在 ES5 时代解决办法是var that this;或者用map的第二个参数手动传入 this。到了 ES6箭头函数提供了更自然的方案因为箭头函数捕获的是外层multiply作用域里的 this而multiply方法的调用方式是obj.multiply()所以 this 能保住。理解了这一层你就能明白箭头函数在这里的定位它不是给所有函数换了一种写法而是专门用来解决“回调函数需要沿用外层 this”的场景。3. 箭头函数真正改掉的是绑定规则从动态调用变成词法捕获普通函数的 this 是动态绑定调用方式决定 this箭头函数的 this 是词法捕获定义位置决定 this。这个差异怎么理解可以用一个不算严谨但好用的类比普通函数的 this 像是在现场点名谁喊了这个函数函数里的 this 就是谁。箭头函数的 this 像是提前写在档案里的联系人函数在创建的时候就已经知道自己的 this 是谁了之后不管谁调用它都改变不了这个联系人。箭头函数里访问 this本质上是在做变量查找。它先看自己内部有没有 this发现没有就往上层作用域找一层一层找上去直到找到一个有 this 绑定的作用域或者最终找到全局环境。3.1 箭头函数没有 this它沿作用域向上找看一个嵌套例子const obj { name: obj, outer: function () { const inner () { console.log(this.name); }; inner(); } }; obj.outer(); // obj解释这条链路outer是普通函数通过obj.outer()调用所以outer里的 this 是 obj。inner是箭头函数定义在outer内部。inner没有自己的 this访问 this 时向上找找到outer的 this也就是 obj。所以输出obj。如果outer本身也是箭头函数呢const obj { name: obj, outer: () { const inner () { console.log(this.name); }; inner(); } }; obj.outer(); // 大概率不是 obj因为outer是箭头函数它自己也没有 this向上找时找到的是定义outer时所在作用域的 this而对象字面量不构成函数作用域所以通常找到的是全局对象或模块顶层 this。这就是为什么箭头函数不能盲目用在对象方法里。你原本可能只是想换个写法结果它捕获的 this 压根不是对象本身。3.2 call / apply / bind 对箭头函数无效普通函数可以通过 call、apply、bind 临时改变 this。箭头函数做不到。function regular() { console.log(this.name); } const arrow () { console.log(this.name); }; regular.call({ name: tom }); // tom arrow.call({ name: tom }); // 取决于定义 arrow 时外层的 this反正不是 tom箭头函数被 call 时第一个参数会被忽略因为它根本没有自己的 this 可以绑定。这个特性在工程上有一个价值当你拿到一个函数想通过 bind 保险地确保它拿到某个 this 时你要先确认它是不是箭头函数。如果是箭头函数bind 是无效的。你只能通过修改接收参数来传递上下文。注意这也意味着如果你在写公共函数时用了箭头函数使用方就不能通过 call 或 apply 来传入自定义 this。对于工具函数和需要灵活上下文的 API 设计这是一个很重要的限制。3.3 箭头函数与普通函数的本质差异对比表维度普通函数箭头函数this 来源每次调用时动态绑定定义时词法捕获能否被 call / apply / bind 改变 this可以不行能否作为构造函数可以不行是否有 arguments 对象有没有是否适合做对象方法适合this 指向对象不适合this 不指向对象是否适合做回调函数需要额外处理 this适合自动捕获外层 this把这张表摆在面前很多问题的答案就自然浮出来了。4. 为什么对象方法里用箭头函数反而更糟三个高频误区很多人对箭头函数的理解停留在“箭头函数不会丢 this”于是到处用。结果在真实项目里箭头函数反而制造了更隐蔽的问题。4.1 误区一对象方法里用箭头函数const user { name: tom, greet: () { console.log(this.name); } }; user.greet(); // 大概率不是 tom原因前面说过了对象字面量不产生函数作用域箭头函数会向更外层捕获 this。在浏览器环境里这里通常拿到的是 window在 ES module 环境下拿到的是模块顶层 this通常是 undefined。对象方法想要访问对象本身必须用普通函数。4.2 误区二DOM 事件回调里默认用箭头函数在 DOM 事件里普通函数的 this 有一个很实用的特性this 指向绑定事件的元素。button.addEventListener(click, function () { console.log(this); // button 元素 });如果改成箭头函数this 变成了定义时外层的 this不再是按钮元素button.addEventListener(click, (e) { console.log(this); // 外层 this不是按钮元素 console.log(e.currentTarget); // 如果想拿按钮元素要用 currentTarget });这不代表箭头函数不能用于事件回调而是说你要清楚用了箭头函数之后就不能依赖 this 拿当前元素了必须显式用事件参数里的e.currentTarget。4.3 误区三类的原型方法写成箭头函数在类里如果这样写class Counter { count 0; increment () { this.count; }; }这种写法叫类字段箭头函数每次实例化都会创建一个新的 increment 函数。它确实可以解决“方法被解构出来调用时 this 丢失”的问题因为 this 在定义时就被绑定到实例上了。但如果你把它理解成“类的所有方法都应该用箭头函数”就会遇到两个问题类字段箭头函数是实例属性不是原型方法。N 个实例就有 N 个函数对象内存上有额外开销。有些场景你并不想让 this 被永久绑定。比如在父类中定义的方法可能希望子类重写后 this 指向更具体的实例。更常见的写法是普通原型方法 需要时在构造函数里 bindclass Counter { count 0; increment() { this.count; } } // 在需要保证 this 不丢失的场景再绑定 const counter new Counter(); const fn counter.increment.bind(counter);类字段箭头函数也有它的适用场景尤其在 React 类组件的函数式组件迁移时代不少代码都这么写。但要清楚它是在用内存换便利不是没有成本。5. 一套 4 步判断法把 this 问题从玄学变成选择判断题到底什么时候用普通函数什么时候用箭头函数不用死记硬背按下面这个顺序做判断。5.1 第一步先确认当前函数是怎么被调用的先问自己这个函数写完之后是直接被调用还是会被当作回调传给某个 API如果是对象方法、构造器、事件回调通常需要一个动态 this。如果是传给 setTimeout、Promise、数组方法、事件监听器等调用方式不由你控制。这一步的目的是判断 this 的稳定程度。凡是“调用方式不确定”的函数普通函数的 this 就不可靠。5.2 第二步判断你需要的到底是调用者还是外层上下文如果函数需要访问调用它的人比如对象方法里的对象本身、DOM 事件里的当前元素用普通函数。如果函数需要访问定义它时外层环境里的 this比如 setTimeout 回调里需要访问外层 Vue 组件实例、类实例用箭头函数。这个问题的核心是函数接收 this 信息的方式是靠“调用时传入”还是靠“定义时捕获”。5.3 一张决策表常见场景与推荐写法场景推荐写法理由对象方法普通函数this 需要动态指向对象本身方法内部嵌套回调箭头函数捕获外层方法作用域的 this构造函数 / class 构造函数普通函数箭头函数不能做构造函数DOM 事件回调需要当前元素普通函数或箭头函数 e.currentTargetthis 不再指向元素类实例方法普通原型方法必要时 bind避免每实例创建新函数需要 arguments 对象普通函数或箭头函数 rest 参数箭头函数没有 arguments需要被 call / apply / bind 灵活传参的工具函数普通函数保留动态绑定能力明确只想从外层作用域读 this箭头函数语义符合需求这张表基本覆盖了日常开发里 80% 的 this 场景。5.4 排查链路this 指向不对时按这个顺序查如果真的遇到 this 不对不要急着改代码按下面顺序排查看当前函数是普通函数还是箭头函数。如果是普通函数确认它的调用方式是直接调用、对象方法、new、还是回调。如果是箭头函数沿词法作用域向上找最近的普通函数作用域确认那个函数里的 this 是什么。如果向上找了半天都找不到普通函数那 this 就是全局或模块顶层 this。确认之后再判断这个 this 是不是你期望的。如果不是回到上面的决策表选另一种写法。这个排查链路的最后一步往往是很多人漏掉的你想要的到底是调用者还是外层上下文有些时候代码没有错只是你的预期错了。6. 落地到真实项目的几条建议箭头函数、普通函数与团队规范理解了机制最后还要谈一谈工程化。因为项目越大代码风格和 this 的使用方式越要统一否则不同开发者的写法混在一起排查起来非常痛苦。6.1 不要让箭头函数成为默认选项我见过一些团队规范为了追求代码简洁规定“回调一律用箭头函数”。这个规定本身没有大问题但如果被理解为“所有函数都优先用箭头函数”就会在对象方法、类方法、公共 API 设计上出问题。更合理的默认策略是普通函数作为默认写法适合方法定义。在需要捕获外层 this 的回调场景里主动切换成箭头函数。在需要动态 this 的场景里坚持普通函数。一句话箭头函数是“有目的的选择”不是“默认的写法”。6.2 长期维护理解机制比记忆规则更可靠规则是可以被打破的但机制不会变。你记不住所有边角规则但只要理解了“普通函数的 this 由调用方式决定箭头函数的 this 由定义位置决定”就能推导出所有结论。这就像学车。你不需要背下每一个路口怎么转弯你只需要理解转向灯、方向盘和车距的关系就能应对新路况。this 也是同样理解调用上下文和词法环境的关系比背诵 20 条规则有用得多。6.3 团队协作时增加一个简单约定在项目里我一般会建议团队统一一个判断标准如果函数内部需要用到 this先开会讨论它是“调用者导向”还是“定义位置导向”。实在不行在 ESLint 配置里可以开prefer-arrow-callback但要注意这会影响 React 类组件里的事件绑定写法。更合适的是配合no-this-assignment等规则防止团队里出现大量var that this的旧式代码。最终你会发现this 问题之所以让很多人头疼不是因为它难而是因为 JavaScript 在语言设计上把函数和 this 分得太开。箭头函数实际上是在这个设计上打了一个补丁让一部分函数不再动态决定 this而是从词法环境里借一个过来用。所以下一次遇到 this 不对不要条件反射式地改成箭头函数。先停下来问自己一个问题这个函数真正需要的是调用它的那个对象还是定义它的那个上下文回答完这个问题你的 this 问题就已经解决了一大半。
返回列表