
四年前我第一次在团队里给人讲JavaScript的this台下有个同事直接举手“你别讲规则了就告诉我写了ES6箭头函数之后是不是就永远不用再担心this了”我当场没回答因为这个问题本身就是个坑。后来我做技术分享、带新人、面试候选人几乎每次都会碰到关于this的讨论。从ES5到ES6this的规则并没有消失只是多了箭头函数这个“特例”。这篇文章我打算把ES5的完整绑定规则、ES6箭头函数的行为差异、真实项目里的踩坑场景放在一起讲如果你能把这篇消化掉以后看到this就不会再靠猜了。1. ES5里this的指向到底由什么决定很多人记this的方式是“谁调用它this就指向谁”。这句话在ES5的大多数场景下能成立但一旦遇到回调函数、事件绑定、变量提取它就失灵了。原因很简单this的绑定发生在调用阶段而不是定义阶段。你得看“这个函数到底以什么方式被调起来”而不是看“它在哪段代码里被写出来”。ES5里完整的this绑定规则一共四条默认绑定、隐式绑定、显式绑定、new绑定。一条条拆开看比背口诀靠谱得多。1.1 默认绑定函数裸调用的兜底规则当一个函数不带任何修饰、直接调用时就是默认绑定。非严格模式下这个this指向全局对象。浏览器里是windowNode.js里是global。严格模式下默认绑定的this是undefined。function showThis() { console.log(this); } showThis(); // 浏览器非严格模式下输出 window如果文件开了use strict同样一行代码输出的是undefined。这里有个特别容易被忽略的坑默认绑定到底是不是严格模式取决于函数体内部的this有没有处于严格模式而不是调用方有没有严格模式。比如你从一个非严格模块里调用一个严格模块里的函数该函数的this依然是undefined。实际开发中我习惯把严格模式下this undefined当成一道保护线。很多框架的源码里会写if (this undefined) throw new TypeError(...)目的就是防止函数被裸调后污染全局状态。1.2 隐式绑定对象方法调用时最常见的this隐式绑定发生在“通过对象的属性去调用函数”这种形态里比如obj.method()。此时this指向那个对象。const user { name: 张三, getName() { return this.name; } }; console.log(user.getName()); // 张三这个规则很好理解但真正的坑往往出现在“隐式丢失”。举个例子const fn user.getName; fn(); // undefinedthis不再指向user你以为只是把方法存到一个变量里但它本质上变成了“函数裸调用”this就落回了默认绑定。类似的情况还出现在回调传参的时候setTimeout(user.getName, 100); // 执行时this不是user这背后的原因很直白user.getName()里user是引用载体setTimeout(user.getName)里引用的是函数本身user这个上下文没有被传过去。我踩过最惨的一次是前端用对象封装了一批接口请求方法然后在数组循环里把方法当成回调传给了事件监听器。结果方法内的this全程指向window所有接口参数全变成undefined。后来我给对象方法统一补了bind问题才消失。所以记住只要方法脱离了原本的对象引用隐式绑定就失效。1.3 显式绑定call、apply、bind统一接管this显式绑定是ES5里最可靠的控制手段三个函数都是用来指定this的区别只在传参方式和后处理方式上。fn.call(thisArg, arg1, arg2, ...)调用函数同时指定this参数逐个传。fn.apply(thisArg, argsArray)调用函数同时指定this参数通过类数组或数组传。fn.bind(thisArg, ...boundArgs)不调用函数返回一个全新函数这个函数无论之后怎么被调用this都会被锁定为thisArg。前两个是“临时认领”第三个是“永久绑定”。比如function introduce(company, years) { console.log(${this.name} 在 ${company} 工作了 ${years} 年); } const person { name: 李四 }; introduce.call(person, 某科技公司, 3); introduce.apply(person, [某科技公司, 3]); const boundIntroduce introduce.bind(person); boundIntroduce(某科技公司, 3);这里有个实用细节bind返回的新函数不能通过再次bind改变指向。对一个已绑定的函数再做bind第一次的this仍然生效。我还遇到过有人用bind去“预填参数”这个特性倒是很好用相当于柯里化的一部分function multiply(a, b) { return a * b; } const double multiply.bind(null, 2); console.log(double(5)); // 101.4 new绑定构造函数里this指向新对象当函数前面加了new调用函数内部会发生一套完整的构造流程。this指向新创建的实例对象默认情况下函数return的原始值会被忽略返回的是这个新对象。function Person(name) { this.name name; } const p1 new Person(王五); console.log(p1.name); // 王五有一个“冷知识”是如果构造函数手动返回了一个对象那么这个对象会替代this成为new表达式的结果。ES5年代经常有人靠这个特性实现“更安全的构造器”但我建议你在日常代码里不要这么写可读性差坑多。1.5 四条规则的优先级怎么排序一旦函数调用同时符合多条规则就得按优先级判断。ES5里确定的顺序是new绑定 显式绑定 隐式绑定 默认绑定所以常见的判断思路是这样的const obj { name: obj, getName() { return this.name; } }; const newObj new obj.getName(); // new绑定生效 obj.getName.call(someOther); // call显式绑定生效 obj.getName(); // 隐式绑定生效 const fn obj.getName; fn(); // 默认绑定生效我也见过一些写得很绕的“孤儿代码”把new、call、bind、对象调用全揉在一起面试的时候确实能考倒人但日常开发基本不会遇到。你只要掌握优先级规则后面无论代码多绕都能拆出结果。2. ES6箭头函数为什么让this“变简单”了ES6引入箭头函数最大的变化并不是“简写”而是它直接废掉了自己绑定this的能力。箭头函数内部没有独立的this它的this是在定义时从外层作用域捕获的而且是词法上的捕获。换句话说箭头函数里的this就是它在写代码时所在那个作用域的this。const user { name: 赵六, sayLater() { setTimeout(() { console.log(this.name); }, 100); } }; user.sayLater(); // 赵六因为sayLater是普通方法调用时this指向user箭头函数定义在sayLater内部所以它捕获了user这个this。如果是普通函数setTimeout里的this就会丢。2.1 箭头函数捕获this的方式更像变量查找很多人把箭头函数理解成“this永远不变”这个说法非常危险。它并不是“锁死”了一个值而是像普通变量一样沿着作用域链向上查找。function outer() { return () { console.log(this); }; } const objA { fn: outer() }; const objB { fn: outer.call(objB) };第一次调用outer()外层this是默认绑定的window箭头函数捕获的就是window之后无论你怎么用objA.fn()调用它都不会变成objA。第二次调用outer.call(objB)外层this是objB箭头函数捕获的就是objB。所以箭头函数里的this不是一个“常量”它只是跟随外层函数当时的this取值。外层this变了它也会跟着变。2.2 箭头函数与普通函数的差异不止this除了this箭头函数还有几个硬性差异很容易在实际项目里触发报错它没有arguments对象。如果在箭头函数里写arguments拿到的是外层函数的arguments不是自己的。它不能当构造函数不能用new调用。它没有prototype属性。它内部没有super绑定。它不能被call/apply/bind改写this因为根本没有自己的this。传进去的thisArg会被忽略。第5条的存在意味着即使你写了arrowFn.call(obj)箭头函数里的this还是原来的捕获值。这不是bug而是语言设计上的取舍要么让你自由绑定要么让你词法捕获不能两全。const getGlobal () this; console.log(getGlobal.call({ name: 无效 })); // 依旧是外层this不是这个对象2.3 箭头函数也解决不了的方法this归属箭头函数最大的副作用是会误导一部分开发者在“对象方法”里也使用它然后踩进另一个坑。const user { name: 钱七, intro: () { return 我是${this.name}; } }; user.intro(); // 我是undefined原因很简单这个箭头函数定义在对象字面量里但对象字面量本身并不会创建一个“方法作用域”它内部的大括号不是函数作用域箭头函数捕获的是外层作用域的this。在全局环境下定义对象捕获到的就是window。所以我的经验是对象的方法该用普通函数就用普通函数箭头函数主要用于“回调”和“需要保留外层this”的函数表达式而不是作为对象方法、类方法的主体。类方法里的箭头函数属于另一套逻辑我们放到后面细说。3. 实战这些高频场景中this到底指向谁光讲规则不够得回到真实场景里验证。我把日常开发里最容易踩的this场景挑出来逐个说明this到底是谁以及对应的处理方式。3.1 DOM事件监听里的this与currentTarget在原生DOM事件监听中普通函数作为监听器时函数内的this指向绑定事件的元素本身。注意这里不是触发事件的target而是绑定监听器的那个元素两者在事件委托时会不同。document.querySelector(.btn).addEventListener(click, function () { console.log(this); // .btn元素 console.log(event.currentTarget); // .btn元素 console.log(event.target); // 真正被点击的元素 });如果用箭头函数作为监听器this就会变成外层上下文通常是window。很多新手在这里栽跟头是因为看到别人用箭头函数写回调就顺手用在事件监听上结果想通过this获取当前元素时拿到了window。处理方式有两种一种是坚持用普通函数保持this语义另一种是箭头函数里用event.currentTarget替代this。我更推荐后一种尤其当你还需要在外层闭包里使用其他数据时箭头函数反而更顺手。3.2 setTimeout、setInterval和异步回调里的this异步回调是this丢失的重灾区。比如const order { amount: 100, pay() { setTimeout(function () { console.log(支付金额${this.amount}); }, 0); } }; order.pay(); // 支付金额undefinedsetTimeout执行回调时并不会把order作为this传进去普通函数在非严格模式下指向全局对象严格模式下为undefined。解决方案基本就三选一外部变量暂存this比如const self this调用外包一层箭头函数对回调函数执行bind。我个人现在很少用const self this这种方式了。ES6环境下直接箭头函数一行解决代码还干净。但有一种情况要注意如果你写的是一个会被外部动态调用、且无法控制封装的库函数那么bind可能比箭头函数更稳妥因为箭头函数会提前把this锁成定义时的值而bind是在函数准备好之后再指定上下文。3.3 对象方法链式调用和“方法提取”的坑对象方法连续调用时this的隐式绑定依然成立const calculator { total: 0, add(n) { this.total n; return this; }, multiply(n) { this.total * n; return this; }, getTotal() { return this.total; } }; calculator.add(5).multiply(3).getTotal(); // 15这种链式写法依赖每个方法返回this。但如果你把add方法提取出来单独调用this就丢了。项目里常见的问题是从模块对象里解构出方法然后当成独立函数使用。const { add } calculator; add(5); // TypeError或者this指向全局解决方案是在模块导出或对象定义阶段就把方法绑定到固定this或者干脆不用对象方法而是用独立函数显式参数。这个设计决策越早做越好否则代码库大了以后到处是“丢失this再想办法救回”的补丁。3.4 类class中this的绑定与事件回调ES6的class本质上还是函数this的规则和ES5构造函数一脉相承但有一个额外的特点类声明和类体的代码默认处于严格模式。这导致类方法被裸调时this不是window而是undefined一调用就会报错反而不容易“静默出错”。class Counter { constructor() { this.count 0; } increment() { this.count; } } const counter new Counter(); const increment counter.increment; increment(); // TypeError: Cannot read property count of undefinedReact开发里最常见的做法是给方法手动绑定this或者直接用类字段箭头函数。箭头函数作为类字段时它捕获的是实例构造上下文里的this也就是实例本身因此可以安全地作为事件回调传出去。class Counter { count 0; increment () { this.count; }; }这种写法的代价是每个实例都会单独创建一份函数不像原型方法那样全局共享内存上略高。但对于组件类应用来说这种代价通常可以接受换来的是事件回调不丢this。如果你的场景极其追求性能且不需要把方法传给外部那就还是用普通方法加bind。3.5 Map、数组遍历回调中的thisArgES6的Map、Set以及数组的某些遍历方法都支持传入第二个参数作为回调的thisArg。比如Map.prototype.forEach(callback, thisArg)。很多人不知道这点以为回调里this只能用箭头函数解决。const map new Map([[name, 周八]]); const handler { prefix: 用户, show(key, value) { console.log(${this.prefix}${value}); } }; map.forEach(handler.show, handler); // 用户周八这里如果不传第二个参数handlerhandler.show被取出来以后this就是undefined。我和同事写过一版数据清洗代码里面大量使用了arr.forEach当时统一走了箭头函数后来需要动态改变上下文才注意到forEach原生支持thisArg反而比bind更轻量。碰到这类方法时先查一下API签名能少写不少冗余代码。4. 用call、apply、bind精准控制this的实战技巧前面的场景说明了一个现实即使你理解规则代码在传递过程中还是会有无数种办法让this丢失。所以工程上真正需要的是“主动控制this”的能力。这一章我系统讲一下call、apply、bind在不同场景下的取舍以及常见误用。4.1 call与apply的参数差异和经典用法call和apply本质一样差别只在参数传递效率。需要传参数量少、且能逐个列出时用call参数本身就以数组形式存在时用apply。一个经典应用是“借用数组方法”。比如要把类数组arguments转成真数组最原始的方式是function toArray() { return Array.prototype.slice.call(arguments); }这里的关键是slice本意是按索引复制数组它并不关心this是不是真正的Array实例只要对象有length和数字索引就能工作。这种“鸭子类型”的思路在JavaScript里特别常见。ES6以后我更喜欢Array.from(arguments)可读性更强。但同类“借用API”的思想依然有用比如字符串方法借用、正则方法借用等。还有一个常见的apply用法不用循环求数组最大值。const nums [1, 3, 2, 8, 5]; Math.max.apply(null, nums); // 8ES6里可以写成Math.max(...nums)更简洁。但如果你在维护老代码看到apply别慌它的作用就是“把数组展开成参数列表”。4.2 偏函数利用bind预置this和参数bind不只是绑this它还支持预置参数。这个能力在处理事件处理器时特别好用比如同一个函数要响应不同状态function updateState(type, event) { console.log(触发了${type}event是${event.type}); } document.querySelector(.btn1).addEventListener(click, updateState.bind(null, 类型A)); document.querySelector(.btn2).addEventListener(click, updateState.bind(null, 类型B));注意这里bind的第一个参数是null因为原函数根本不依赖this。预置参数后事件对象会自动作为最后一个参数传进来。这个模式在开发组件库、工具函数时非常常见比包一层箭头函数更省内存因为每次bind都会返回一个固定函数不会每次render都重新创建。4.3 多次bind和丢失bind的迷思一个我经常在面试里问的问题函数先bind一次再bind一次this到底是谁答案是最先绑定的this。因为第一次bind返回的已经是一个bound function它的this被锁死了第二次bind试图再去改这个bound function的this不会生效。function sayName() { return this.name; } const objA { name: A }; const objB { name: B }; const boundA sayName.bind(objA); const boundB boundA.bind(objB); console.log(boundB()); // A别把bind理解成“包裹一层还能再改”它的实现逻辑是第一次bind后的函数内部替身会记录[[BoundThis]]之后无论如何调用都直接使用这个值。但是有一个细节如果第一次bind的this值是null之后是否可以再次生效要看运行环境新版JavaScript里bound函数再次绑定依然不会改写原始锁定的this值。另一种迷思是认为bind之后的函数没有prototype、不能new。事实上bound function也可以被new这时原始函数的构造逻辑会生效而不是把this锁到绑定的对象上。这种场景日常非常少见知道就行别用来写骚操作。4.4 什么时候该用箭头函数什么时候该用bind我的选择标准很清楚你只是想“继承外层this”回调嵌套层级多希望写起来自然首选箭头函数。你需要在函数创建之后、某个确定时机再指定上下文或者需要重复复用一个绑定函数并传递出去用bind。你需要临时调用一次并传参用call或者apply。你在写一个不依赖this的纯函数三种都可以但尽量保持纯函数风格不要依赖this。另外不要在对象方法上用箭头函数去“解决this”那是南辕北辙。对象方法丢失this的根源往往是“方法被提取后调用”解决之道是bind或者箭头函数做包装不是把方法本身改成箭头函数。5. 面试和日常中判断this的一套心法最后这套心法是我这几年面试候选人和自己排查bug时反复验证出来的。遇到一个带this的函数先别慌按下面流程一步一走基本不会判断错。5.1 四步定位法看函数本身是不是箭头函数。如果是箭头函数不去看调用形态直接向上找外层作用域的this。如果不是箭头函数看它是怎么被调用的new调用就查构造函数this指向新对象。有没有call、apply、bind有则this指向显式传入的对象。如果前三条都不成立再看它是不是通过某个对象属性调用的obj.fn()是则this指向obj否则默认绑定非严格模式是全局对象严格模式是undefined。这个顺序不仅适用于读代码也适用于调试。哪一步卡住了就在这里打断点或者打印日志比满屏console.log瞎猜高效得多。5.2 三道高频题验证一下题一var name globalName; const obj { name: objName, getName() { return this.name; } }; const fn obj.getName; console.log(fn());答案globalName。因为fn是裸调用隐式绑定丢失落到默认绑定。注意非严格模式下this是全局对象取到全局name。题二const length 10; function fn() { console.log(this.length); } const obj { length: 5, method(fn) { fn(); arguments[0](); } }; obj.method(fn);这里第一行fn()是裸调用非严格模式下this是全局对象length是10第二行arguments[0]()属于隐式绑定this指向arguments这个类数组对象arguments.length是2因为method传了一个参数。所以输出两个值10和2。这道题考的是“arguments也是对象方法从里面取出来再调用会绑定arguments”。题三const obj { name: obj, says: () console.log(this.name) }; obj.says();答案取决于外层通常是undefined或全局name。因为箭头函数不绑定obj它捕获全局作用域的this。别把对象方法直接箭头化。这三道题覆盖了默认绑定、隐式绑定、词法绑定三类情况每次面试我都喜欢拿着三类组合出题能刷掉一批背口诀的候选人。5.3 项目里的三条约定代码规范比个人理解更重要。我带的项目里通常约定普通方法定义统一用“方法简写语法”对象和类里不要用箭头函数定义方法。类的实例方法如果会被解构或作为回调统一在构造器里bind或者使用类字段箭头函数但要团队内统一风格。回调函数的this需要继承外层时统一箭头函数不要用const that this这种老写法。凡是与DOM事件绑定的回调优先使用event.currentTarget不要依赖普通函数的this语义这样函数体可以被安全提取和复用。这些约定不是凭空拍脑袋每一类问题都对应上面讲过的某个坑。团队里只要共同遵守this相关的bug会减少一大半。5.4 我自己的分析与调试习惯我调试this相关问题时最常用的工具不是console.log一个this就完事而是在函数执行那一步打印调用栈。Chrome DevTools里每个栈帧会显示当前这个函数是怎么被调起来的只要点开Call Stack就能直观看到“是obj.method调用还是method裸调用”。另外遇到隐蔽的this问题我会临时把函数内容整体包一层Function.prototype.call去调整调用形态快速验证当前this到底是谁。比如写const suspectedThis { name: 测试 }; fn.call(suspectedThis);如果函数能正常运行说明它内部确实依赖this那么问题就出在“传入的上下文不对”如果换成call也没用说明问题根本不是this而是函数内部其他变量作用域出错。这种二分排查法比疯狂加日志要快很多。说到最后this的真正难点不在于规则多而在于JavaScript的函数“调用形态”多。ES6箭头函数不是消灭了this而是给了你一条绕开动态绑定的路径。我的个人建议是先精通ES5的默认绑定、隐式绑定、显式绑定和new绑定再去用箭头函数的词法绑定这两个知识体系缺一不可。你越是能准确说出“这一分this是哪里来的”后面的代码就越不容易出暗病。