
先说我前阵子遇到的一个线上问题。同事写了一个进度条组件类里面起了一个setInterval(function(){ this.update(); }, 2000)结果控制台一直报this.update is not a function。他第一反应是怀疑update方法命名错了翻来覆去找了十分钟。我过去看了一眼直接把this打印出来发现它根本不是组件实例而是undefined——这就不是方法不存在的问题是this在定时器回调里彻底丢了。这类问题几乎每个写 JavaScript 的人都遇到过。市面上多数教程只会给你一句this 指向调用它的对象这句话撑不过三种场景就会翻车回调、解构、事件监听。this 真正决定方式是执行上下文在函数调用那一刻动态设置的一个绑定值也就是标题里说的执行上下文决定的动态绑定机制。这篇博文我从执行上下文讲起拆开四条绑定规则、箭头函数的词法 this再给几个我实际排查过的丢 this 案例最后分享我这些年沉淀的判断流程。适合刚在 this 上翻过车的人也适合准备面试时被问到this 原理的开发者。1. 先弄懂 this 的出生地每次函数调用都搭一个执行上下文1.1 执行上下文里存了哪些现场信息JavaScript 引擎执行代码不是逐行傻跑而是遇到一次函数调用就为这次调用创建一个执行上下文Execution Context。这个上下文就是引擎为函数调用准备的运行现场里面至少装三样东西变量环境记录var声明、函数声明的存放位置词法环境记录let/const、闭包作用域链ThisBinding就是当前上下文里this到底绑到谁身上。浏览器开发者工具里打断点时右侧 Scope 面板能展开Local、Global、Closure等层级那就是当前执行上下文的可视化呈现。而 V8 引擎里那个著名的调用栈Call Stack本质上就是执行上下文栈——每次函数调用往栈里压一个执行上下文函数执行完弹出去。this 就存在栈顶那个执行上下文的 ThisBinding 槽位里。你可以把函数定义理解成菜谱执行上下文理解成厨房。菜谱写清楚了要放哪些食材、按什么顺序炒但菜谱上不会写主厨是谁。只有菜真正下锅的那一刻厨房里站着谁谁就是主厨。this 就是那个主厨——函数写完之后它自己是不知道 this 是谁的得等真正执行的那一刻才定。1.2 变量靠词法作用域this 靠调用点这是动态绑定的本质JavaScript 里有一个很深的设计差异普通变量的作用域在代码解析阶段就确定了这叫词法作用域。代码还没跑起来你光看函数嵌在哪里就能判断它内部能访问哪些外层变量。但this不行。this是在运行期由**调用点Call Site**决定的。调用点就是函数在代码里被调用的那一行。同一个函数放在不同对象上或者用不同方式调用this可能完全不同function whoIsThis() { console.log(this); } const a { method: whoIsThis }; const b { method: whoIsThis }; a.method(); // 输出 a 对象 b.method(); // 输出 b 对象 whoIsThis(); // 浏览器里输出 window严格模式下输出 undefined同一个whoIsThis三个调用点三个 this。这就解释了为什么this 指向定义它的对象这个说法是错的——函数在定义时根本没有绑定任何对象它只是一个可以挂到任何对象上的普通函数。所谓动态绑定就是指 this 的绑定动作发生在运行时执行上下文的创建过程中而不是发生在代码编译期。理解了这一点后面四条绑定规则才能串起来。它们本质上回答一个问题函数在被调用的那一刻执行上下文引擎是根据什么规则去填 ThisBinding 这个槽位的。2. 四条绑定规则从默认到 new一条条拆开看2.1 默认绑定直接调用时 this 要么是全局对象要么是 undefined最基础的一条规则当函数以裸调用的方式执行也就是前面没有obj.、没有new、没有call/apply/bind的时候执行上下文会做默认绑定。非严格模式下this 默认指向全局对象。浏览器里是windowNode.js 里是global。严格模式下use strictthis 是undefined引擎不会给你一个全局对象兜底。use strict; function showThis() { console.log(this); // undefined } showThis();这里有个高频误区严格模式下 this 变成null不是严格模式下裸调用的 this 就是undefined。它只是说找不到可绑定的调用对象时不再好心给你指向全局。还有一类场景特别容易踩外层函数的 this 不会自动传给内层函数。const obj { name: obj, outer() { function inner() { console.log(this); // 不是 obj是 window 或 undefined } inner(); } }; obj.outer();在inner()里你写this不会拿到obj因为inner自己是裸调用没有隐式绑定规则参与。很多老代码里的var self this或var that this就是为了绕开这个坑——在外层把 this 存下来内层用闭包变量访问。2.2 隐式绑定obj.method() 背后的引用和丢失链当函数以对象方法的形式被调用比如obj.method()执行上下文就会把 this 绑到这个对象上。这个规则叫隐式绑定。const user { name: Nora, greet() { console.log(Hello, ${this.name}); } }; user.greet(); // Hello, Nora这里面有几个必须知道的细节第一链式调用只看最后一段。a.b.c.method()里的 this 是c不是a也不是b。因为 JavaScript 方法调用的优先级里紧贴方法名左侧的那个引用对象才是 this 的来源。第二隐式绑定最容易丢。你把方法从一个独立的引用里取出来再调用隐式绑定立刻失效退化成默认绑定const user { name: Nora, greet() { console.log(this.name); } }; const fn user.greet; // 方法引用被解构出来 fn(); // TypeError: Cannot read properties of undefined (reading name) // 非严格模式下 this 是 windowwindow.name 通常为空字符串严格模式下直接报错这就是隐式丢失。它发生在三类高频场景里变量解构const { greet } user; greet();函数传参setTimeout(user.greet, 1000)或把方法作为回调传给框架数组方法回调松散调用[1,2,3].forEach(user.greet)里回调内部的 this 也不是 user想传 this 得用第二个参数。那句经典的 this 指向调用它的对象 之所以会骗人正是因为方法引用传给回调后调用它的人已经变了。引用还是同一个函数但调用点换了this 就换人。2.3 显式绑定call/apply/bind 的使用与坑如果不想依赖谁调用谁决定JavaScript 提供了三个能直接指定 this 的 APIcall、apply、bind统称显式绑定。call(thisArg, arg1, arg2, ...)立即调用参数平铺传递apply(thisArg, argsArray)立即调用参数以数组传入bind(thisArg, arg1, ...)返回一个新函数这个函数体内的 this 被永久固定不立即执行。function showTitle() { console.log(this.title); } const video { title: 执行上下文详解 }; const guide { title: 绑定规则避坑 }; showTitle.call(video); // 执行上下文详解 showTitle.apply(guide); // 绑定规则避坑 const boundShow showTitle.bind(video); boundShow(); // 执行上下文详解 boundShow.call(guide); // 还是执行上下文详解bind 之后 call 改不回去使用时的三个注意点bind 是硬绑定。bind 返回的新函数之后你再对它 call/apply 传别的 this都会被忽略它只认当初 bind 时指定的对象。这个特性在事件处理器、定时器回调里非常实用。传 null 或 undefined 的坑。fn.call(null)在非严格模式下this 会回落成全局对象只有严格模式下才会保持null本身。以前有人拿 null 当占位符结果函数内部无意中访问/修改了全局对象排查起来非常费劲。数组方法自带 thisArg 参数。forEach、map、filter这些方法的第二个参数可以传入一个对象作为回调里的 thisconst counter { step: 2 }; [1, 2, 3].map(function (n) { return n * this.step; }, counter); // [2, 4, 6]不过在现在更推荐直接用箭头函数后面讲。2.4 new 绑定最高优先级连 bind 都能压过去用new调用一个函数时执行上下文按一套固定流程创建创建一个全新的空对象把这个对象的原型链接到构造函数的prototype上把构造函数内部的this绑定到新创建的对象上执行构造函数内部的代码如果函数显式返回了一个对象则返回那个对象否则返回新对象。第 3 步就是 new 绑定的核心。所以构造函数里的this指向的是 new 出来的那个实例对象function User(name) { this.name name; } const u new User(Nora); console.log(u.name); // Nora当多条规则同时出现时优先级非常关键。可以记这个顺序new 绑定 显式绑定 隐式绑定 默认绑定有个面试常考的压轴例子new和bind谁优先function Person(name) { this.name name; } const BoundPerson Person.bind({ name: default }); const p new BoundPerson(Alice); console.log(p.name); // Alice答案是 new 赢了。bind返回的新函数被new调用时引擎会忽略 bind 时设置的 this转而去构造一个新对象并把它作为 this 传给原构造函数。所以p.name是Alice而不是default。这个点知道的人不多但也正是this 由执行上下文决定而不是由某个语法形式表面决定的最好证明。3. 箭头函数为什么能救场又为什么不能乱用3.1 词法 this箭头函数把 this 从运行时搬回了定义时箭头函数和普通函数最大的区别只有一句话箭头函数没有自己的 this 绑定。它不会参与前面四条绑定规则中的任何一条引擎也不会为它单独设置 ThisBinding。箭头函数内部的this是在定义它的时候从外层作用域继承下来的——记住这是词法规则不是动态规则。回到开头的进度条问题用箭头函数非常干净class Progress { start() { setInterval(() { this.update(); // 这里的 this 是 start 方法里的 this }, 2000); } update() { // 刷新逻辑 } }start()被实例调用时start 自身执行上下文里的 this 是实例。箭头函数定义在 start 内部所以它的 this 就捕获了 start 的 this跟setInterval怎么触发它完全无关。定时器回调哪怕脱到全局环境里执行箭头函数内的 this 依然是那个实例。因为箭头函数没有自己的 this所以它天然不能做构造函数没有prototype属性也不能用new调用。对这个箭头函数执行call、apply、bind指定 this 是无效的——参数可以正常传但thisArg会被忽略。这一点在写工具函数时尤其要注意别指望 bind 能改箭头函数的 this。3.2 对象方法、构造函数里的箭头函数三个场景三种结局箭头函数好归好但千万不能无脑替换普通 function。最容易翻车的场景是对象字面量。const obj { name: obj, getName: () this.name }; obj.getName();对象字面量本身不产生函数作用域所以箭头函数写在对象里时它捕获的是定义位置的外层 this。这段代码在浏览器全局作用域里执行this就是windowwindow.name通常是空字符串在 ES 模块或严格模式下this是undefined直接报错读不出name。总之拿不到obj.name。再看构造函数内部定义箭头函数字段的情况class Counter { constructor() { this.count 0; this.increment () { this.count; }; } } const counter new Counter(); const fn counter.increment; // 即使解构出来再调用 fn(); console.log(counter.count); // 1这里能成功是因为箭头函数定义在构造函数执行上下文中外层函数的 this 是 new 出来的实例所以它捕获的就是实例 this。即便你把这个方法解构出来传给回调它的 this 也不会丢。这就是 React 类组件时代handleClick () {}这种写法的底层原理。所以判断方法很简单箭头函数适合做回调、做记住 this的辅助函数不适合做对象字面量的方法、不适合做原型方法。对象方法想让 this 跟着调用对象走老老实实用普通函数表达式或 ES6 方法简写。4. 实战复盘三次this 丢失的完整排查过程4.1 setTimeout 回调里的 this三步定位模拟现场组件里有一段进度轮询逻辑期望每隔两秒调用一次实例方法更新数据。function Poller() { this.count 0; } Poller.prototype.start function () { setInterval(function () { this.count; // TypeError: Cannot read properties of undefined console.log(this.count); }, 2000); };排查链路按这三步走第一步确认报错对象不是方法本身。在回调第一行写console.log(this)发现是undefined严格模式或window非严格模式说明 this 根本不是 Poller 实例方法名没写错。第二步找到调用点。定时器的回调是由事件循环在到达时间后触发的触发方式就是拿到这个函数裸调用。它前面没有poller.也没有new所以默认绑定生效。回调在定义时挂在start方法内但start的 this 不会自动传导给它。第三步选一种修复方式。三种都可以// 方案一箭头函数捕获 this Poller.prototype.start function () { setInterval(() { this.count; }, 2000); }; // 方案二bind 固定 this Poller.prototype.start function () { setInterval(function () { this.count; }.bind(this), 2000); }; // 方案三外层缓存 thisES5 老做法 Poller.prototype.start function () { const self this; setInterval(function () { self.count; }, 2000); };我自己的习惯是新代码优先箭头函数旧代码一遇到self或that就格外警惕。self能解决问题但也在暗示这里的 this 语义已经不够清晰了。4.2 DOM 事件监听里 this 飘忽不定跨浏览器老问题还在addEventListener里的事件回调this 默认指向绑定元素本身看起来像隐式绑定const button document.getElementById(saveBtn); button.addEventListener(click, function () { console.log(this); // button 元素 });但它有隐蔽的变数同一个函数绑定到多个元素this 会跟着元素变。如果你需要固定的 this要么用箭头函数要么先 bind。旧浏览器时代attachEvent的回调里 this 是window所以要兼容老 IE 时代码往往写成event.currentTarget或手工闭包。现在虽然在现代化浏览器里用addEventListener就好但到处拿 this 当元素用的习惯仍可能埋坑。function onClick() { console.log(this.id); } const a document.getElementById(btnA); const b document.getElementById(btnB); a.addEventListener(click, onClick); // 输出 btnA b.addEventListener(click, onClick); // 输出 btnB // 想要固定场景 const boundOnClick onClick.bind({ id: fixed }); a.addEventListener(click, boundOnClick); // 输出 fixed事件场景下的更稳妥写法是在回调里用event.currentTarget而不是this。因为currentTarget是事件对象上的属性不依赖 this 绑定语法语义更明确。跨端环境、React 合成事件、小程序事件里只认event.currentTarget的习惯能少踩很多坑。4.3 快速判断 this 的排查清单和调试技巧面对一段不确定 this 的代码按下面这个顺序判断每一步都问是不是这个函数是箭头函数吗是 → 直接找它定义位置的外层 this不用看调用方式它是不是被new调用了是 → this 是 new 创建的新对象它有没有被bind过有 → this 是 bind 时传入的对象除非后来又 new调用它的时候是不是以call/apply形式是 → this 是第一个参数严格模式下传什么就是什么调用点是不是obj.method()形式是 → this 是方法名左侧的 obj以上全否 → 裸调用严格模式 undefined非严格模式全局对象。整理成表更直观调用形式this 指向箭头函数定义位置外层作用域的 this定义时定格new fn()new 创建的新对象fn.bind(obj)obj之后再 call/apply 改不掉fn.call(obj)/fn.apply(obj)obj本次调用生效obj.fn()obj裸调用fn()严格模式 undefined非严格模式全局对象调试上我的习惯是在报错处往回找调用点在调用点那一行打断点看调用栈和 Scope 面板里最顶层的 this 值。永远不要靠背代码去猜打印 this比读代码快得多。当你看到this.xxx is not a function这类报错时第一反应不是去检查xxx方法有没有拼错而是先确认this是谁——大部分时候问题出在 this 而不是方法名。5. 写代码这些年我养成的 this 使用习惯5.1 写一个函数之前先问自己五个问题我给自己定了一条规矩每个函数在被定义时都要预判它未来会怎么被调用。具体就问五句这个函数会被当作构造函数new吗这个函数会被传给 setTimeout、事件监听、Promise 回调这类调用者不是自己的地方吗这个函数会不会被解构出来脱离对象单独调用这个函数定义的地方外层有没有我真正想捕获的 this这个问题用箭头函数更简单还是用普通函数更合理只要第 2 或第 3 个问题的答案是会我就会在定义处直接做防护要么用箭头函数要么bind(this)要么干脆不在函数内部使用this而是把需要的对象属性通过参数传进来。这条习惯帮我省掉了大量线上排查时间因为绝大多数 this 问题都是可以提前在写法上消灭的。5.2 class 场景下消除 this 焦虑的实践类方法天然是普通函数所以如果用类组织代码this 丢失问题更常见。我的实践偏好新代码优先用类字段箭头函数class Counter { count 0; increment () { this.count; }; } const counter new Counter(); const fn counter.increment; fn(); // count 变成 1旧代码在构造函数里统一 bindclass Counter { constructor() { this.count 0; this.increment this.increment.bind(this); } increment() { this.count; } }两者达到的效果一样前者更简洁。需要说明的是类字段语法是基于现代 JavaScript 标准的一个附加特性如果项目运行环境较旧且没有做编译降级建议用构造函数 bind 的方案。事件回调里尽量少用 this。拿 React 类组件举例onClick{this.handleClick}这个写法里React 内部调用回调时并不会帮你绑定组件实例所以类组件里必须bind或用类字段箭头函数。函数组件时代虽然不碰 class this 了但本质问题还在——你传出去的是一个方法引用引用本身不携带调用对象。5.3 面试被问 this 时怎么答到点子上如果面试官问说说 JavaScript 里的 this我会按这个顺序在脑子里组织this 不是定义时决定的它是执行上下文里的 ThisBinding由函数被调用那一刻的调用点决定这就是动态绑定四条绑定规则默认绑定、隐式绑定、显式绑定、new 绑定优先级从低到高默认绑定在严格模式和非严格模式下结果不同一个是 undefined一个是全局对象隐式绑定最脆弱方法引用一旦被解构或透传就退化为默认绑定显式绑定用 call/apply/bindbind 是硬绑定new 可以覆盖 bind箭头函数是例外没有自己的 this定义时捕获外层 this也不能 new。面试官如果继续追问new 和 bind 谁优先级高就把前面的BoundPerson例子摆出来。追问严格模式下 call 传 null 会怎样就要回答严格模式下 this 保持 null非严格模式会回落成全局对象。这些点我一个都不打算背模板——因为它们都能从执行上下文决定 this这个源头推导出来。理解了源头任何变体问题都只是换了个场景的同一道题。最后说一句个人体会。很多人把 this 当成一个需要死记的行为细节我的感受恰恰相反它是 JavaScript 里最值得花时间理解透的机制之一。因为 once 你搞懂了 this 和执行上下文的关系再看作用域闭包、事件循环、类继承都会顺畅很多——前端核心概念是连在一起的。哪怕你平时写代码几乎不主动用 this你也会在接手别人的项目、读框架源码、排查诡异 bug 时反复和它打交道。把这些规则变成肌肉记忆回报率很高。