Javascript篇 | this指向与执行上下文
前言
「this指向」是前端面试的高频考点,也是实际开发中的常见陷阱。很多同学都有这样的体验:
- 写代码时感觉没问题,运行起来this就指错了
- 面试时被问到this,脑子里一团浆糊
- 箭头函数和普通函数的this总是搞混
今天我们就用一道经典面试题,彻底搞懂JavaScript中this的所有玩法
诊断自测
在开始之前,试着回答以下问题,检测你当前对 this 的理解程度:
1. 以下代码输出什么?为什么?
var x = 10;
const obj = {
x: 20,
getX: function() {
return this.x;
}
};
const retrieveX = obj.getX;
console.log(retrieveX());
2. 下面两个 console.log 分别输出什么?
function Timer() {
this.seconds = 0;
setInterval(function() {
this.seconds++;
console.log(this.seconds); // A
}, 1000);
}
const t = new Timer();
3. 以下代码中,this.color 输出什么?
const car = {
color: 'red',
getColor: () => {
return this.color;
}
};
console.log(car.getColor());
点击查看答案
第1题: 输出 10(浏览器环境)或报错(严格模式)。retrieveX 是一个独立调用的函数引用,发生了隐式绑定丢失,this 指向全局对象。
第2题: A 处不断输出 NaN。setInterval 的回调是普通函数且被独立调用,this 指向全局对象(window),window.seconds 是 undefined,undefined + 1 = NaN。
第3题: 输出 undefined。箭头函数没有自己的 this,它继承定义时外层的执行上下文。对象字面量 {} 不构成执行上下文,所以 this 指向全局对象,而全局对象上没有 color 属性。
如果三道题都答对了,说明你基础不错!继续阅读可以查漏补缺。如果有答错的,别担心,读完本文你就能彻底搞懂。
先来个灵魂拷问👇
var name = 'window';
function Person(name) {
this.name = name;
this.show1 = function() {
console.log(this.name);
};
this.show2 = () => {
console.log(this.name);
};
this.show3 = function() {
setTimeout(function() {
console.log(this.name);
}, 0);
};
this.show4 = function() {
setTimeout(() => {
console.log(this.name);
}, 0);
};
}
var personA = new Person('personA');
var personB = new Person('personB');
// 请写出以下代码的输出结果
personA.show1();
personA.show1.call(personB);
personA.show2();
personA.show2.call(personB);
personA.show3();
personA.show3.call(personB);
personA.show4();
personA.show4.call(personB);
看完之后是不是有点懵?别急,读完这篇文章,你不仅能轻松解答这道题,还能彻底搞懂JavaScript中this的所有玩法。
从规范说起:this到底是什么?
要想真正理解this,我们得先从ES规范说起。
在ECMAScript规范中,this是执行上下文的一个属性。每当代码执行进入一个新的执行上下文时,this就会被重新绑定。
听起来有点抽象?我们换个说法:
this的值不是在函数定义时确定的,而是在函数被调用时动态绑定的。
这就解释了为什么同一个函数,在不同的调用场景下,this会指向不同的对象。
执行上下文是什么?
执行上下文(Execution Context)是JavaScript代码运行的环境。主要分为两种:
- 全局执行上下文:代码首次运行时创建,只有一个
- 函数执行上下文:每次函数调用时创建,可以有多个
每个执行上下文都有自己的this绑定,这就是我们要研究的重点。
全局上下文中的this
这是最简单的场景。
console.log(this); // 浏览器环境: window
在全局上下文中:
- 浏览器环境:this指向
window对象 - Node.js环境:this指向
global对象
没什么好说的,记住就行。
函数上下文中的this:五种绑定规则
重头戏来了!函数中的this绑定有五种情况,我们一个一个来看。
默认绑定
当函数独立调用时,就会触发默认绑定。
var name = 'window';
function sayName() {
console.log(this.name);
}
sayName(); // 'window'
规则:
- 非严格模式:this指向全局对象(window)
- 严格模式:this为
undefined
'use strict';
function sayName() {
console.log(this); // undefined
}
sayName();
💡 记忆技巧:直接调用函数,没有任何”主人”,this就只能找全局对象了。
隐式绑定
当函数作为对象的方法被调用时,this指向调用该方法的对象。
const person = {
name: 'Alice',
sayName: function() {
console.log(this.name);
}
};
person.sayName(); // 'Alice'
看起来很简单?但有个陷阱:
const person = {
name: 'Alice',
sayName: function() {
console.log(this.name);
}
};
const fn = person.sayName;
fn(); // 'window' 或 undefined(严格模式),因为这个时候的代码执行上下文变成了全局!
为什么?
因为fn只是拿到了函数的引用,调用fn()时已经不是通过person.sayName()的形式了,变成了独立调用,触发的是默认绑定。
⚠️ 隐式绑定丢失是面试高频考点!记住:关键看调用时的形式,而不是定义时。
显式绑定:call / apply / bind
有时候我们希望手动指定this的值,这时就要用到call、apply、bind。
call
function greet() {
console.log(`Hello, ${this.name}`);
}
const person1 = { name: 'Alice' };
const person2 = { name: 'Bob' };
greet.call(person1); // 'Hello, Alice'
greet.call(person2); // 'Hello, Bob'
语法: func.call(thisArg, arg1, arg2, ...)
apply
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
const person = { name: 'Alice' };
greet.apply(person, ['Hello', '!']); // 'Hello, Alice!'
语法: func.apply(thisArg, [argsArray])
bind
function greet() {
console.log(`Hello, ${this.name}`);
}
const person = { name: 'Alice' };
const boundGreet = greet.bind(person);
boundGreet(); // 'Hello, Alice'
语法: const boundFunc = func.bind(thisArg, arg1, arg2, ...)
三者对比
| 方法 | 是否立即执行 | 参数传递方式 | 返回值 |
|---|---|---|---|
| call | 是 | 逐个传递 | 函数执行结果 |
| apply | 是 | 数组形式 | 函数执行结果 |
| bind | 否 | 逐个传递 | 新函数 |
💡 记忆口诀:call和apply立即执行,bind返回新函数;call逐个传参,apply数组传参。
new绑定
使用new调用构造函数时,this会绑定到新创建的对象上。
function Person(name) {
this.name = name;
this.sayName = function() {
console.log(this.name);
};
}
const person = new Person('Alice');
person.sayName(); // 'Alice'
💡 核心:new会创建新对象,并让this指向它。
箭头函数:特立独行的存在
箭头函数是ES6引入的新特性,它的this绑定规则完全不同。
核心规则:箭头函数没有自己的this,它的this继承自定义时所在的外层执行上下文。
const obj = {
name: 'object',
regularFunc: function() {
console.log('regularFunc:', this.name);
},
arrowFunc: () => {
console.log('arrowFunc:', this.name);
}
};
obj.regularFunc(); // 'regularFunc: object'
obj.arrowFunc(); // 'arrowFunc: undefined' (继承自全局执行上下文)
更重要的是:箭头函数的this一旦确定就无法改变。
const obj = {
name: 'object',
arrowFunc: () => {
console.log(this.name);
}
};
const anotherObj = { name: 'another' };
obj.arrowFunc.call(anotherObj); // 依然是undefined,无法改变
让我们再看一个更清晰的例子:
function Person(name) {
this.name = name;
// 普通函数,this由调用时决定
this.regularFunc = function() {
console.log(this.name);
};
// 箭头函数,this继承自Person构造函数的this
this.arrowFunc = () => {
console.log(this.name);
};
}
const person = new Person('Alice');
person.regularFunc(); // 'Alice'
person.arrowFunc(); // 'Alice'
const anotherObj = { name: 'Bob' };
person.regularFunc.call(anotherObj); // 'Bob' - 可以改变
person.arrowFunc.call(anotherObj); // 'Alice' - 无法改变
⚠️ 重点:箭头函数的this在定义时就已经确定,它会捕获外层执行上下文的this,之后无论如何调用都不会改变。
绑定优先级
当多种绑定规则同时出现时,优先级如何?
优先级从高到低:
- new绑定
- 显式绑定(call/apply/bind)
- 隐式绑定
- 默认绑定
function foo() {
console.log(this.name);
}
const obj1 = { name: 'obj1', foo: foo };
const obj2 = { name: 'obj2' };
// 隐式绑定 vs 显式绑定
obj1.foo.call(obj2); // 'obj2' - 显式绑定优先
// 显式绑定 vs new绑定
const boundFoo = foo.bind(obj1);
const instance = new boundFoo(); // undefined - new绑定优先
特殊情况: 箭头函数不参与优先级比较,因为它的this完全由定义时的外层执行上下文决定。
综合实战:解答开头的面试题
现在我们有了完整的知识体系,可以轻松解答开头的题目了。
var name = 'window';
function Person(name) {
this.name = name;
this.show1 = function() {
console.log(this.name);
};
this.show2 = () => {
console.log(this.name);
};
this.show3 = function() {
setTimeout(function() {
console.log(this.name);
}, 0);
};
this.show4 = function() {
setTimeout(() => {
console.log(this.name);
}, 0);
};
}
var personA = new Person('personA');
var personB = new Person('personB');
show1
personA.show1(); // 输出: 'personA'
personA.show1.call(personB); // 输出: 'personB'
分析:
show1是普通函数- 第一次调用:隐式绑定,this指向
personA - 第二次调用:显式绑定,this被改为
personB
show2
personA.show2(); // 输出: 'personA'
personA.show2.call(personB); // 输出: 'personA'
分析:
show2是箭头函数,定义在Person构造函数内- this继承自
Person构造函数的执行上下文,在new Person('personA')时this绑定为personA - 箭头函数的this无法通过
call改变,所以两次都是'personA'
show3
personA.show3(); // 输出: 'window'
personA.show3.call(personB); // 输出: 'window'
分析:
show3内部的setTimeout使用了普通函数作为回调setTimeout的回调函数是独立调用的,触发默认绑定- 非严格模式下,this指向
window - 即使用
call改变了show3的this,也不影响内部setTimeout回调的this
show4
personA.show4(); // 输出: 'personA'
personA.show4.call(personB); // 输出: 'personB'
分析:
show4内部的setTimeout使用了箭头函数作为回调- 箭头函数的this继承自外层(show4函数的执行上下文)
- 第一次:
show4的this是personA,箭头函数继承,输出'personA' - 第二次:
call改变了show4的this为personB,箭头函数继承,输出'personB'
Bonus1: 类中的 this
class C {
instanceField = this;
static staticField = this;
}
const c = new C();
console.log(c.instanceField === c); // true
console.log(C.staticField === C); // true
前面说了这么多,为什么没有提到类中的 this 呢?你可能注意到,类中的 this 语义好像和前面讨论的都不太一样。那是不是应该也存在一种”类上下文”呢?
其实不需要。类本质上是一种特殊的”函数”,它使用的仍然是我们前面讨论的函数上下文。类只是构造函数的一层语法糖包装,所以会让你感觉类中的 this 好像不一样了。
让我们简单验证一下类确实是函数:
class C {}
console.log(typeof C); // "function"
console.log(C.prototype.constructor === C); // true
// 类的实例字段初始化,等价于在构造函数中赋值
class C1 {
instanceField = this;
}
// 等价于:
function C2() {
this.instanceField = this;
}
当你写 instanceField = this 时,这个初始化器会在构造函数执行时运行,此时的 this 遵循 new 绑定规则,指向新创建的实例。
而 static staticField = this 则是在类定义时执行,此时 this 指向类构造函数本身。
所以类中的 this 并没有引入新的绑定规则,它只是在不同位置应用了我们已经熟悉的函数上下文中的 this 绑定机制。
Bonus2:eval中的this
掌握了上面的this绑定知识,可能对于通过面试已经够用了。但如果想惊艳到面试官,那么这个冷门的this绑定也必须学!
eval是JavaScript中一个比较特殊的存在,它能够执行字符串形式的代码。而在eval中,this的绑定也有特殊的规则。
直接调用 vs 间接调用
eval有两种调用方式,它们对this的影响完全不同:
const obj = {
name: 'object',
test: function() {
// 直接调用eval
eval('console.log(this.name)'); // 'object'
// 间接调用eval
(0, eval)('console.log(this)'); // 'window'
}
};
obj.test();
看到区别了吗?同样是调用eval,结果却天差地别。
什么是直接调用?
所谓直接调用,就是直接使用eval(...)的形式。这种情况下,eval中的代码会继承当前执行上下文的this。
function foo() {
eval('console.log(this)');
}
const obj = { name: 'object' };
foo.call(obj); // 输出: obj对象
你可以把直接调用的eval理解为:这段字符串代码就像是直接写在当前位置一样,它”看到”的this就是外层的this。
什么是间接调用?
间接调用是指通过其他方式调用eval,常见的有:
// 方式1:通过逗号运算符
(0, eval)('console.log(this)');
// 方式2:赋值给变量
const myEval = eval;
myEval('console.log(this)');
// 方式3:通过window调用
window.eval('console.log(this)');
间接调用时,eval中的代码会在全局执行上下文中执行,因此this指向全局对象。
const obj = {
name: 'object',
directEval: function() {
eval('console.log(this.name)'); // 'object'
},
indirectEval: function() {
(0, eval)('console.log(this.name)'); // undefined
}
};
obj.directEval();
obj.indirectEval();
为什么会有这样的设计?
这其实是ECMAScript规范的一个设计细节。直接调用eval时,它需要访问当前的执行上下文(包括变量、this等);而间接调用时,为了安全性和一致性,规范规定它应该在全局上下文中执行。
再看一个更复杂的例子:
var name = 'global';
const obj = {
name: 'object',
test: function() {
var name = 'local';
// 直接调用:能访问局部变量和当前this
eval('console.log(name, this.name)'); // 'local', 'object'
// 间接调用:只能访问全局,this指向window
(0, eval)('console.log(name, this.name)'); // 'global', 'global'
}
};
obj.test();
看出来了吗?直接调用的eval就像透明的一样,它完全融入了当前的执行环境;而间接调用则是独立的,始终在全局环境中执行。
常见误区
学完了这么多规则,我们来看看大家在理解 this 时最容易踩的坑,帮你避免”以为自己懂了”的尴尬。
误区一:this 指向函数本身
很多从其他语言转来的同学,看到 this 这个词会本能地以为它指向函数本身。毕竟 “this” 嘛,“这个函数”,听起来很合理?
function counter() {
this.count++;
}
counter.count = 0;
counter();
counter();
console.log(counter.count); // 0,没有增加!
实际上 counter() 是独立调用,this 指向的是全局对象,this.count++ 操作的是 window.count,而不是 counter.count。this 永远不会指向函数自身,除非你显式地用 call(counter) 来绑定。
误区二:对象方法中嵌套的函数,this 会指向该对象
这是隐式绑定丢失的经典场景。很多同学以为只要函数写在对象的方法内部,this 就会指向那个对象。
const team = {
name: 'Frontend',
members: ['Alice', 'Bob'],
printMembers: function() {
this.members.forEach(function(member) {
console.log(`${member} belongs to ${this.name}`);
});
}
};
team.printMembers();
// Alice belongs to undefined
// Bob belongs to undefined
forEach 里的回调函数是独立调用的,触发默认绑定,this 指向全局对象。要解决这个问题,可以用箭头函数、bind,或者在外层缓存 const self = this。
误区三:箭头函数的 this 指向定义时所在的对象
很多人把”定义时确定”误解为”定义在哪个对象里,this 就指向哪个对象”。实际上箭头函数继承的是外层执行上下文的 this,而对象字面量 {} 不会创建新的执行上下文。
const config = {
env: 'production',
getEnv: () => {
return this.env;
}
};
console.log(config.getEnv()); // undefined,不是 'production'
这里箭头函数定义在对象字面量内部,但对象字面量不是执行上下文,所以 this 继承的是外层(全局)的 this。只有函数调用(包括构造函数/类)才会创建新的执行上下文。
误区四:bind 之后还能再被 call/apply 改变 this
一旦通过 bind 绑定了 this,后续的 call、apply 甚至再次 bind 都无法覆盖第一次的绑定。
function sayHi() {
console.log(`Hi, ${this.name}`);
}
const boundToAlice = sayHi.bind({ name: 'Alice' });
boundToAlice.call({ name: 'Bob' }); // 'Hi, Alice',不是 'Hi, Bob'
const rebind = boundToAlice.bind({ name: 'Charlie' });
rebind(); // 'Hi, Alice',依然是 Alice
bind 产生的绑定是”硬绑定”,一旦确定就不会被后续的显式绑定所覆盖。这个特性在面试中经常被用来出题。
总结
核心要点
优先级: new绑定 > 显式绑定 > 隐式绑定 > 默认绑定
核心原则:
- this不是在编写时绑定,而是在运行时绑定
- 关键看调用位置和调用方式
- 箭头函数是特例,它的this在定义时就确定了,继承自外层执行上下文
- eval的直接调用和间接调用对this的影响不同
记忆技巧
- 默认绑定:独立调用,找全局
- 隐式绑定:谁调用指向谁,但容易丢失
- 显式绑定:手动指定,强制绑定
- new绑定:创建新对象,指向新对象
- 箭头函数:定义时确定,继承外层,无法改变
掌握了这些,无论遇到多复杂的this问题,你都能游刃有余地解决!
练习挑战
光看不练假把式,来试试这几道题,检验一下学习成果。
挑战一:基础(⭐)
请写出以下代码的输出结果:
var age = 100;
const user = {
age: 25,
getAge: function() {
return this.age;
},
getAgeArrow: () => {
return this.age;
}
};
const { getAge, getAgeArrow } = user;
console.log(user.getAge()); // A
console.log(getAge()); // B
console.log(user.getAgeArrow()); // C
console.log(getAgeArrow()); // D
答案与解析
- A:
25— 隐式绑定,user.getAge()中 this 指向 user。 - B:
100— 解构赋值后独立调用,隐式绑定丢失,触发默认绑定,this 指向全局对象,window.age = 100。 - C:
100— 箭头函数的 this 继承自定义时的外层执行上下文(全局),对象字面量不创建执行上下文,所以 this 是 window。 - D:
100— 同 C,箭头函数的 this 不受调用方式影响,始终是全局对象。
挑战二:进阶(⭐⭐)
请写出以下代码的输出结果:
const factory = {
label: 'Factory',
create: function() {
return {
label: 'Product',
getLabel: function() {
console.log(this.label);
},
getLabelArrow: () => {
console.log(this.label);
}
};
}
};
const product = factory.create();
product.getLabel(); // A
product.getLabelArrow(); // B
const product2 = factory.create.call({ label: 'Custom' });
product2.getLabelArrow(); // C
答案与解析
- A:
'Product'—product.getLabel()是隐式绑定,this 指向 product 对象,其 label 是'Product'。 - B:
'Factory'—getLabelArrow是箭头函数,定义在create函数的执行上下文中。factory.create()调用时 this 是 factory(隐式绑定),所以箭头函数继承的 this 是 factory,输出'Factory'。 - C:
'Custom'—factory.create.call({ label: 'Custom' })通过 call 将 create 的 this 改为{ label: 'Custom' }。箭头函数继承的就是这个被改变的 this,输出'Custom'。
这道题的关键在于理解:箭头函数继承的是外层函数调用时的 this,而不是外层对象。如果外层函数的 this 被 call/apply 改变了,箭头函数继承的也是改变后的值。
挑战三:综合(⭐⭐⭐)
请写出以下代码的输出顺序和结果:
var id = 'Global';
function Article(id) {
this.id = id;
}
Article.prototype.getId = function() {
return this.id;
};
Article.prototype.getIdLater = function() {
setTimeout(function() {
console.log('A:', this.id);
}, 0);
setTimeout(() => {
console.log('B:', this.id);
}, 0);
const boundFn = function() {
console.log('C:', this.id);
}.bind(this);
setTimeout(boundFn, 0);
};
const article = new Article('Article-1');
const getId = article.getId;
console.log('D:', article.getId());
console.log('E:', getId());
article.getIdLater();
答案与解析
同步输出(按代码顺序):
- D:
'Article-1'—article.getId()隐式绑定,this 是 article。 - E:
'Global'—getId是独立调用,隐式绑定丢失,this 指向全局对象。
异步输出(setTimeout 回调,按注册顺序):
- A:
'Global'— setTimeout 的普通函数回调独立调用,默认绑定,this 是全局对象。 - B:
'Article-1'— 箭头函数继承外层getIdLater的 this,而article.getIdLater()是隐式绑定,this 是 article。 - C:
'Article-1'— 通过 bind 硬绑定了getIdLater的 this(即 article),所以输出 article 的 id。
完整输出顺序:
D: Article-1
E: Global
A: Global
B: Article-1
C: Article-1
这道题综合考察了隐式绑定、隐式绑定丢失、默认绑定、箭头函数继承、bind 硬绑定,以及同步异步的执行顺序。如果你全部答对了,说明你对 this 的理解已经非常扎实了!
自我检测
读完本章后,确认你能回答以下问题:
- 能说出 this 的五种绑定规则(默认绑定、隐式绑定、显式绑定、new 绑定、箭头函数)
- 能说出绑定的优先级顺序:new > 显式 > 隐式 > 默认
- 能解释什么是”隐式绑定丢失”,以及它在什么场景下会发生
- 能解释为什么箭头函数的 this 无法被 call/apply/bind 改变
- 理解对象字面量
{}不会创建执行上下文,所以箭头函数不会把 this 绑定到对象本身 - 能区分 call、apply、bind 的区别(是否立即执行、参数传递方式)
- 能解释 bind 产生的硬绑定为什么无法被后续的 call/apply 覆盖
- 了解 eval 直接调用和间接调用对 this 的不同影响
- 能解释类中的 this 为什么本质上还是函数上下文的 this
购买课程解锁全部内容
大厂前端面试通关:71 篇构建完整知识体系
¥89.90