写这篇文章之前,我先说个真实的感受:去年前端团队招人,我面了三十多个候选人,只要问到“this指向”这个题,能一次说清楚的不超过五个。很多人背了各种规则,一到具体代码就懵,最后干脆用“箭头函数解决问题”来搪塞。其实this这套机制没那么玄,核心就一句话——它指向什么,取决于函数被谁调用、怎么调用,而不是函数定义在哪。能把这句话参透,所有面试题都只是套公式。
这篇文章我会把所有this绑定的场景拆开揉碎,从底层规则讲到实际项目里的坑,最后配一套完整面试题解析。不管你是刚入门的小白,还是准备跳槽想查漏补缺,都能在这篇里找到答案。
1. this的本质:不是“指向自己”,而是“调用时的上下文”
1.1 先破除最常见的误解
很多初学者看到this这个单词,第一反应是“指向当前函数自己”。这是最深的误解。函数在JavaScript里也是对象,函数调用自身完全可以用函数名直接引用,跟this没关系。
我组里有个新同事写过一段代码:
javascript复制function count() {
this.count++;
}
count.count = 0;
for (let i = 0; i < 5; i++) {
count();
}
console.log(count.count); // 输出什么?
他以为能输出5,结果输出0。因为this.count++里的this压根不是count函数对象,在非严格模式下它指向全局对象,等于给全局加了个NaN属性,跟count.count毫无关系。这就是“this指向自身”这个说法最大的坑。
1.2 this到底是什么
准确地说,this是函数执行时生成的一个“上下文引用”,它指向的还是调用这个函数的“主体对象”。你可以把它理解成一句话里的“我”——“我”是谁,取决于谁在说这句话。
- 张三说“我今天吃了火锅”,“我”指张三;
- 李四说“我今天吃了火锅”,“我”指李四。
函数定义就像这句“我今天吃了火锅”,本身没有固定的“我”,只有执行的时候才知道。JavaScript里决定this指向的,就是调用方式。这是理解全篇的基础,后面的所有场景,本质上都是“调用方式”的变体。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 四大绑定规则:所有this场景的总纲
2.1 规则一:默认绑定——普通函数调用
当函数以最“裸”的方式调用,不带任何修饰,比如:
javascript复制function hello() {
console.log(this);
}
hello();
这段代码在浏览器非严格模式下,this指向window;在Node.js环境里指向global;如果代码文件里开了"use strict",则this为undefined。
这个规则看着简单,但很容易被套进坑里。很多人以为函数在对象里定义,this就指向对象,其实不然。看这个:
javascript复制const obj = {
name: 'obj',
getThis: function() {
return this;
}
};
const fn = obj.getThis;
console.log(fn()); // 指向全局,不是 obj
obj.getThis拿出来单独赋值给fn,调用fn()就是裸调用,this就丢了。调用点决定了this,而不是定义位置。
还有一个特别容易翻车的场景:函数作为参数传给setTimeout、forEach这类高阶函数。比如:
javascript复制const obj = {
data: [1, 2, 3],
process: function() {
this.data.forEach(function(item) {
console.log(this); // this 是全局,不是 obj
});
}
};
在forEach的回调里,普通函数是裸调用,this是全局对象。这一点经常被忽略,等看到this.data是undefined的时候才意识到问题。
解决方法也不复杂,要么用箭头函数,要么在forEach里传第二参数:
javascript复制this.data.forEach(function(item) {
console.log(this);
}, this); // 第二个参数作为回调内的 this
2.2 规则二:隐式绑定——看调用点有没有“宿主对象”
如果一个函数作为某个对象的方法来调用,比如obj.method(),那么函数里的this指向这个obj。这就是规则二:谁调用,指向谁。
但隐式绑定里的坑特别多。最经典的一个:方法被引用出来再调用,this就丢了。
javascript复制const user = {
name: '小明',
greet: function() {
console.log('你好,我是' + this.name);
}
};
const greetFn = user.greet;
greetFn(); // 输出“你好,我是undefined”
原因就是调用点变成了裸调用,默认绑定规则生效。这和上面提到的fn = obj.getThis是同一个坑,只是换了个马甲。
另一个常见场景是回调函数里使用对象方法:
javascript复制const user = {
name: '小红',
greet: function() {
console.log('你好,我是' + this.name);
}
};
function runCallback(cb) {
cb(); // 等价于裸调用,this 丢失
}
runCallback(user.greet); // 输出“你好,我是undefined”
传入runCallback时,user.greet只是被当作函数值传进去,没有任何对象在调用它,所以this丢失。很多事件监听、定时器回调、Promise回调里的this问题,根源都是它。
那怎么保住this呢?常见方案有三个:
-
用
bind显式绑定:javascript复制runCallback(user.greet.bind(user)); -
在外面套一层箭头函数:
javascript复制runCallback(() => user.greet()); -
在传入前就把
this固化到闭包里:javascript复制const self = this; setTimeout(function() { self.doSomething(); }, 1000);
面试时如果问到“怎么保证回调里的this不丢”,基本就是考察这几种方案的差异。
2.3 规则三:显式绑定——call、apply、bind的底层逻辑
显式绑定指的就是通过call、apply、bind手动指定函数的this。三者用起来有细微差别:
fn.call(context, arg1, arg2, ...):立即执行,参数逐个传。fn.apply(context, argsArray):立即执行,参数以数组形式传。fn.bind(context):返回一个新函数,不立即执行,之后调用永远绑定这个context。
一个典型面试题是手写bind,考察你对this机制的理解。可以这样实现:
javascript复制Function.prototype.myBind = function(context, ...outerArgs) {
const fn = this;
return function(...innerArgs) {
return fn.apply(context, [...outerArgs, ...innerArgs]);
};
};
这里核心就是myBind返回的包裹函数,在调用时通过apply显式把this锁死成context。这里还有个细节:如果bind被用来作为构造函数调用(即new),原生的bind会失效,this指向新对象。但手写实现如果没处理new,绑定依然生效,这属于进阶考点,面试能说清楚会加分。
2.4 规则四:new绑定——优先级最高的一档
javascript复制function Person(name) {
this.name = name;
}
const p = new Person('张三');
用new调用函数时,this指向新创建的空对象,然后函数体内的this就绑定在这个对象上,最后如果函数没有显式返回对象,new会把那个对象返回。
这里容易被问到的一个点是:构造函数里this是什么时候绑定上去的?其实处理是在函数执行前就完成了——new本质上执行了四步:
- 创建新的空对象;
- 设置新对象的原型为构造函数的
prototype; - 将
this指向新对象,执行构造函数; - 如果构造函数返回引用类型(对象/函数),返回它;否则返回新对象。
所以下面的题是经典陷阱:
javascript复制function Car() {
this.brand = 'Tesla';
return { brand: 'BYD' };
}
const car = new Car();
console.log(car.brand); // BYD
因为构造函数显式返回了一个对象,new的结果会被这个对象覆盖。这个知识点在“模拟实现new”的面试题里会用到。
2.5 优先级总结:遇见同时满足多条规则的代码怎么判
实际代码里,可能有多种规则都适用。比如:
javascript复制const obj1 = { name: 'obj1' };
const obj2 = { name: 'obj2' };
function foo() {
console.log(this.name);
}
foo.call(obj1); // 显式绑定
obj2.foo = foo;
obj2.foo.call(obj1); // 既有隐式绑定,又有显式绑定
const bound = foo.bind(obj1);
const obj3 = { name: 'obj3', bound: bound };
obj3.bound(); // 隐式绑定 vs 显式绑定,谁赢?
判定优先级的标准顺序是:
new绑定最优先;- 显式绑定(
call/apply/bind)其次; - 隐式绑定(对象方法调用)再次;
- 默认绑定(裸调用)垫底。
所以obj3.bound()输出obj1,因为bind返回的包裹函数内部锁死了this,对象调用也改不了。obj2.foo.call(obj1)输出obj1,同理,显式覆盖隐式。
这优先级不只是面试知识点,实际开发里判断代码行为非常有用。遇到“方法被赋值到别的对象上了,this还指原对象吗”这类问题,直接按优先级排就行。
3. 箭头函数的this:不是不绑定,而是没有自己的this
3.1 箭头函数的核心机制:词法作用域继承
箭头函数最容易被误解的地方在于“箭头函数this指向外层”。更准确的说法是:箭头函数没有自己的this,它的this从词法作用域继承。
什么叫词法作用域继承?简单说,箭头函数定义在哪一层作用域,它的this就是那一层的this。这个绑定在函数定义时就确定了,之后任何call、apply、bind都没法改变它。
举个例子:
javascript复制const mainObj = {
name: 'main',
outerFn: function() {
const arrowFn = () => {
console.log(this);
};
arrowFn.call({ name: 'other' }); // 输出什么?
}
};
mainObj.outerFn();
这里箭头函数arrowFn定义在outerFn内部,而outerFn是以mainObj.outerFn()调用的,所以外层的this是mainObj。箭头函数继承的是这个this,即便用call指向别的对象,输出依然是mainObj。
这个特性在实际开发里非常好用。以前写React类组件,事件回调里要写this.onClick = this.onClick.bind(this)或this.onClick = this.onClick.bind(this),用箭头函数就不用绑了:
javascript复制class Button extends React.Component {
handleClick = () => {
console.log(this.props);
};
render() {
return <button onClick={this.handleClick}>点我</button>;
}
}
箭头函数的this在定义时确定,事件回调里拿着这个函数直接用,this不会丢。
3.2 箭头函数在哪些情况下会坑到你
箭头函数好用,但用错场景也会出事。最典型的就是它不适合作为对象方法:
javascript复制const obj = {
data: 'hello',
print: () => {
console.log(this.data);
}
};
obj.print(); // undefined
因为箭头函数定义在obj字面量所在的外层作用域,那个作用域里没有data,this.data当然是undefined。这时候箭头函数帮不上忙,反而把this带跑偏了。
另一个容易踩的坑是在浏览器模块顶层的this。模块顶层作用域this是undefined(不是window),箭头函数定义在顶层,那它的this也是undefined。有人希望它指向window,结果大失所望。
所以“要不要用箭头函数”,核心判断依据是:你需要this跟随调用方式变化,还是希望this固定在外层上下文?对大部分回调场景,箭头函数是救星;对需要动态this的对象方法、构造函数,箭头函数就是炸弹。
4. 实战场景全渗透:事件、定时器、框架里的this
4.1 事件处理函数里的this:DOM元素是王者
浏览器环境中,事件处理函数里的this默认指向绑定事件的DOM元素。看一个再常见不过的例子:
javascript复制document.getElementById('btn').addEventListener('click', function() {
console.log(this); // 按钮元素
});
在原生事件监听里,回调函数里的this指向目标元素,这属于隐式绑定的一种特殊体现。但你要是给回调里套一层箭头函数,this就变了:
javascript复制const app = {
init: function() {
document.getElementById('btn').addEventListener('click', () => {
console.log(this); // app,不是按钮
});
}
};
app.init();
箭头函数把this固定在外层init的上下文,也就是app。想要按钮就写普通函数,想要app就写箭头函数——两者机制完全不同,面试里也常拿这个考。
4.2 定时器和异步回调:this丢失是常态
先看这个经典案例:
javascript复制const counter = {
count: 0,
start: function() {
setInterval(function() {
this.count++;
console.log(this.count);
}, 1000);
}
};
counter.start();
这段代码只会输出NaN。原因在于定时器回调是异步且裸调用的,this指向全局,this.count成了全局的undefined + 1,最终NaN。
如果要修复,有三种方式:
-
用箭头函数:
javascript复制start: function() { setInterval(() => { this.count++; }, 1000); } -
在外面保存
this:javascript复制start: function() { const self = this; setInterval(function() { self.count++; }, 1000); } -
用
bind:javascript复制start: function() { setInterval(function() { this.count++; }.bind(this), 1000); }
三者的本质区别在于:箭头函数定义时就已经把this固定了,bind是显式绑定固定了this,保存self则完全绕开this机制。
4.3 Vue和React里的this,为什么跟原生不一样
先看Vue。Vue 2 + Options API时代,methods里的方法会做一层代理绑定,把this指向组件实例,所以在模板里直接调用方法不会有问题。但如果你把方法单独拆出来监听事件,比如:
javascript复制methods: {
handleClick() {
console.log(this); // 组件实例?
}
},
mounted() {
window.addEventListener('resize', this.handleClick);
}
这里的this.handleClick传给addEventListener后调用时,this可能就不是组件实例了,因为原生事件回调是裸调用。Vue官方也建议这种做法时手动bind或用箭头函数包裹。
Vue 3 + Composition API则没这个问题,因为setup里没有组件实例this这一说,你直接用响应式变量和函数组合逻辑,this的存在感大幅降低。这也是Composition API在工程化里更好用的原因之一。
再看React类组件。早期React类组件里比较经典的场景是:
javascript复制class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
this.increment = this.increment.bind(this);
}
increment() {
this.setState(prev => ({ count: prev.count + 1 }));
}
render() {
return <button onClick={this.increment}>加一</button>;
}
}
onClick触发时,increment是作为回调函数被调用的,this会丢。所以当年写React类组件,几乎每个方法都要bind(this)。后来用箭头函数类属性,或者函数组件配合Hooks,才终于摆脱了这个困扰。
4.4 框架场景里的保底绑定方案对比
结合上面场景,我整理了一张表,方便各位按场景选用:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 原生DOM事件回调里想用元素对象 | 普通函数 | this自动绑定元素 |
| 类组件方法作为事件回调 | 箭头函数类属性 | 定义时固化this,方法引用安全 |
| 定时器/异步回调里引用外部对象 | 箭头函数 | this继承外层上下文 |
| 需要动态改变调用对象 | call/apply/bind |
显式绑定,灵活可控 |
对象方法且依赖this动态变化 |
普通函数 | 箭头函数会继承外层,不适用 |
这里我特别想提醒一句:绑定方案没有银弹,全看“你希望this是动态的还是静态的”。动态场景用普通函数,静态场景用箭头函数或bind,混着用才容易出事。
5. 面试真题全解:从基础到“连环追命问”
5.1 高频基础题:先拿下送分题
第1题:看代码输出
javascript复制var name = 'global';
function get() {
console.log(this.name);
}
const obj = {
name: 'obj',
get: get
};
obj.get();
答案是obj。因为调用点是obj.get(),隐式绑定规则生效,this指向obj。
第2题:换一个变量接收再调用
javascript复制const obj = {
name: 'obj',
get: function() {
console.log(this.name);
}
};
const fn = obj.get;
fn();
浏览器非严格模式下输出global(严格模式下报Cannot read properties of undefined)。因为fn()是裸调用,默认绑定生效。
这两题是一对调换变量以考察对调用点理解的高频组合,不少人第一题对,第二题就错,然后卡在“为什么”上。
第3题:严格模式的坑
javascript复制function show() {
'use strict';
console.log(this);
}
show();
严格模式下输出undefined。这不是Bug,是设计如此——避免无意中污染全局。这个考点经常跟“为什么我把函数改造后this变了”一起出现。
5.2 进阶综合题:多规则叠加后的推算
第4题:bind与对象方法的叠加
javascript复制const obj = { name: 'obj' };
function greet() {
console.log(this.name);
}
const boundGreet = greet.bind(obj);
const parent = {
name: 'parent',
greet: boundGreet
};
parent.greet();
输出obj。原因:bind之后的函数是显式绑定了上下文的,调用它的对象改不了;优先级别上显式绑定大于隐式绑定。
第5题:箭头函数绑定失效
javascript复制const obj = {
name: 'obj',
greet: function() {
return () => this.name;
}
};
const fn = obj.greet();
console.log(fn.call({ name: 'other' }));
输出obj。箭头函数没有自己的this,用call是改不了的,它还是继承greet执行时的外层this(就是obj)。
这题能考察出候选人是否理解“箭头函数在定义时锁定this”这一条关键规则,很多人在fn.call({...})这里会被绕进去。
第6题:new优先于显式绑定
javascript复制function Person(name) {
this.name = name;
}
const bound = Person.bind({ name: 'fake' });
const p = new bound('real');
console.log(p.name);
输出real。因为new优先级最高,bind不会影响构造函数通过new调用时的绑定。这也是手写bind时需要考虑的一个边界条件。
5.3 面试官最爱的“连环追命问”
面试官不会只问一串独立选择题,还喜欢连起来追问:
追问1:obj.method()和const fn = obj.method; fn()的区别是什么?
一个this绑定对象,一个裸调用this丢失。本质上是调用点不同。
追问2:那obj.method.call(window)呢?
输出全局值,因为显式绑定优先级高于隐式绑定。
追问3:箭头函数里可以用call改变this吗?
不能。箭头函数的this在定义时锁定,call/apply/bind无法改变。
追问4:既然箭头函数this改不了,那它适合用在哪些场合?
适合需要稳定上下文的场景:异步回调、事件回调、类属性方法等。不适合需要动态this的场景:普通对象方法、构造函数。
这一串问下来,考察的是你的“规则树”是否完整:先判定优先级,再判断调用点,最后看箭头函数特殊情况。按这个顺序推理,就不会漏项。
6. 常见问题与排查技巧:真遇到this错乱怎么快速定位
6.1 一看this就懵?先画“调用点三要素”
我自己调试this错乱时有一个土办法,见到一行代码先问三个问题:
- 函数怎么被调用的?裸调用、方法调用、
call调用、new调用? - 它是不是箭头函数?如果是,找定义向外一层作用域的
this。 - 有没有被
call/apply/bind包裹过?
把这三个问题回答完,this指向基本就锁定了。很多时候问题出在第三问——某个函数被工具库或框架偷偷bind了,导致你以为是普通函数。
6.2 调试this的3个实用技巧
技巧一:在关键位置打印调用栈
javascript复制function debugThis() {
console.log('this:', this);
console.log('调用栈:', new Error().stack);
}
看调用栈能直接看到“是谁在什么位置调用了这个函数”。很多时候this错乱是因为函数被某个库包装了一层,调用栈一眼能看出问题。
技巧二:用断点看调用点
浏览器DevTools里在函数入口打上断点,执行到断点时看右侧Scope面板,留意是否有This值。如果显示Window而你预期是某个对象,说明this丢在了调用链路上。
技巧三:写个可复用的“this探针”
javascript复制function whatIsThis() {
console.log('当前 this:', this);
console.log('是否为严格模式:', function() {
'use strict';
return !this;
}());
return this;
}
把这个函数丢到任何你没把握的地方调用,输出一目了然。
6.3 我处理过的高频事故清单
根据我多年写业务代码、带新人的经验,this错乱最常见在以下几个地方:
-
事件回调里用了
this,结果指向了DOM元素而非组件实例。特征:this.setState报错,或者this.data是undefined。 -
定时器和
setInterval回调里的this丢失。特征:数据没更新,控制台飘NaN。 -
把类方法从对象里拆出来用,比如
const fn = obj.method,然后直接调用。特征:回调执行时this变成全局或undefined。 -
框架源码里的隐式绑定,比如Vue的
methods、React的setState回调。特征:直接看代码看不出问题,实际上框架内部用call/apply/bind做了包装。 -
箭头函数错误地用在对象方法上,导致
this指向外层而非对象本身。特征:obj.method()输出的是外层上下文数据。
遇到这些情况,别急着改代码,先在可疑函数开头打印一下this,定位是调用链哪一步丢的,再决定用什么方案修复。硬猜和乱加bind只会让代码更乱。
我个人在实际项目里还有一个心得:不要为了“酷”在某些场景强行用箭头函数或bind,代码可读性比“写得很潮”重要得多。this乱,本质上是上下文乱;上下文乱,维护就是个无底洞。把每个回调的this都搞明白、写清楚,比任何快捷键技巧都值钱。
7. 最后送你一套“this速查心法”
如果你不想记太多细节,就记住下面这个精简版判定链:
- 是不是箭头函数?是 → 往定义处的外层作用域找
this。 - 是不是
new调用?是 →this指向新对象。 - 是不是
call/apply/bind调用?是 →this指向你传入的对象。 - 是不是某个对象的方法调用(
obj.method())?是 →this指向obj。 - 都不是 → 默认绑定,非严格模式下是全局对象,严格模式下是
undefined。
按这个顺序往下查,没有查不出来的this。
补充一个小技巧:如果你拿到一段没有this的代码,但需要知道“在这个位置this是谁”,可以在那个位置写一个普通函数然后立即调用:
javascript复制(function() { console.log(this); })();
浏览器控制台里跑一下,当前环境的this就出来了。
最后说一个实战里容易忽略的细节:模块加载器和构建工具的影响。在ES Module和Webpack打包环境下,模块顶层this不是window而是undefined,如果你在一些工具函数或组件里直接用了顶层this,很容易在开发环境正常、线上环境崩溃。所以我个人的建议是:能不用this就尽量不用,非得用的时候先把绑定规则想清楚,再动手写代码。
