文章中英模式
布鲁斯前端JS面试题目 - JavaScript this 关键字深入解析
深入讲解 JavaScript 中 this 关键字的运作原理、绑定规则、箭头函数特性,以及 call、apply、bind 方法的使用场景与最佳实践。
文章中英模式
懒得看文章?那就来看视频吧
this 关键字基本原则
JavaScript 中的 this 是一个特殊关键字,指向函数执行时的「上下文对象」。理解 this 的核心原则是:谁调用函数,this 就指向谁。
this 的五种绑定规则
- •
预设绑定
:直接调用函数时,this 指向全局对象(非严格模式)或 undefined(严格模式) - •
隐含绑定
:作为对象方法调用时,this 指向调用该方法的对象 - •
显式绑定
:使用 call()、apply()、bind() 明确指定 this 的值 - •
new 绑定
:使用 new 关键字调用函数时,this 指向新建立的对象实例 - •
箭头函数绑定
:箭头函数没有自己的 this,使用外层作用域的 this
this 的不同绑定方式详解
1. 预设绑定(Default Binding)
当函数被直接调用时,this 预设指向全局对象(浏览器中为 window,Node.js 中为 global)。在严格模式 ('use strict') 下,预设绑定的 this 为 undefined。
function printThis() {
console.log(this);
}
printThis(); // window 或 global
// 严格模式
function strictFunction() {
'use strict';
console.log(this);
}
strictFunction(); // undefined
2. 隐含绑定(Implicit Binding)
当函数作为对象的方法被调用时,this 指向该对象。这是最常见的 this 绑定方式。
const user = {
name: 'Bruce',
age: 30,
greet() {
console.log(`Hello, I'm ${this.name} and I'm ${this.age} years old`);
},
profile: {
title: 'Developer',
showTitle() {
console.log(`My title is ${this.title}`);
}
}
};
user.greet(); // "Hello, I'm Bruce and I'm 30 years old" (this 指向 user)
user.profile.showTitle(); // "My title is Developer" (this 指向 user.profile)
// 注意:隐含绑定可能丢失
const greetFn = user.greet;
greetFn(); // "Hello, I'm undefined and I'm undefined years old" (this 指向全局对象)
3. 显式绑定(Explicit Binding)
使用 call()、apply()、bind() 明确指定函数执行时的 this 值,不论函数原本如何定义。
function introduce(hobby1, hobby2) {
console.log(`I'm ${this.name}, I like ${hobby1} and ${hobby2}`);
}
const person = { name: 'Bruce' };
// call: 参数逐一传入
introduce.call(person, 'coding', 'reading');
// "I'm Bruce, I like coding and reading"
// apply: 参数以数组形式传入
introduce.apply(person, ['coding', 'reading']);
// "I'm Bruce, I like coding and reading"
// bind: 返回一个新函数,不立即调用
const boundFn = introduce.bind(person, 'coding');
boundFn('reading'); // 可以稍后传入剩余参数
// "I'm Bruce, I like coding and reading"
4. new 绑定(Constructor Binding)
使用 new 关键字调用函数时,会自动执行以下步骤,其中包含将 this 绑定到新建立的对象:
- •创建一个新的空对象
- •将该对象的原型设为构造函数的 prototype 属性
- •将函数内的 this 绑定到新建立的对象
- •执行构造函数中的代码
- •如果函数没有返回其他对象,则返回这个新建立的对象
function Person(name) {
this.name = name;
// this 指向新建立的对象
}
const bruce = new Person('Bruce');
console.log(bruce.name); // "Bruce"
// this 在构造函数中指向新建立的对象实例
// 即使在全局作用域或严格模式下,new 绑定也有效
5. 箭头函数绑定(Arrow Function)
箭头函数没有自己的 this,它会捕获定义时所在的词法环境中的 this 值,而且这个值不会因为调用方式的改变而改变。
const obj = {
name: 'Bruce',
// 传统函数 - this 依调用方式决定
regularMethod() {
console.log(this.name);
},
// 箭头函数 - this 锁定在定义时的环境
arrowMethod: () => {
console.log(this.name);
},
// 常见用法: 在传统方法内使用箭头函数
delayedGreeting() {
setTimeout(() => {
console.log(`Hello, ${this.name}`); // this 仍指向 obj
}, 1000);
}
};
obj.regularMethod(); // "Bruce" (this 指向 obj)
obj.arrowMethod(); // undefined (this 指向定义时的全局环境)
obj.delayedGreeting(); // 1秒后输出 "Hello, Bruce"
// 注意:箭头函数的 this 不受 call/apply/bind 影响
obj.arrowMethod.call({name: 'Not Bruce'}); // 仍然是 undefined
解决 this 问题的常见方式
1. 使用 bind 方法固定 this
在构造函数或类的方法中,可以使用 bind 将方法的 this 显式绑定到实例上。
class Counter {
constructor() {
this.count = 0;
// 将方法的 this 绑定到实例
this.increment = this.increment.bind(this);
}
increment() {
this.count++;
}
getCount() {
return this.count;
}
}
const counter = new Counter();
const incrementFn = counter.increment;
incrementFn(); // 即使脱离对象调用也能正常工作
console.log(counter.getCount()); // 1
2. 使用箭头函数自动绑定
箭头函数会自动捕获定义时的 this 值,是处理回调函数中 this 问题的好方法。
class Counter {
constructor() {
this.count = 0;
}
// 使用箭头函数自动捕获外层 this
increment = () => {
this.count++;
}
getCount() {
return this.count;
}
}
const counter = new Counter();
const incrementFn = counter.increment;
incrementFn(); // 箭头函数保持 this 指向 counter 实例
console.log(counter.getCount()); // 1
3. 使用变量保存 this
在 ES6 之前,常用的方法是在外层函数中将 this 值保存到变量中,然后在内部函数中使用该变量。
function UserProfile() {
this.name = 'Bruce';
this.friends = ['Alice', 'Bob'];
// 使用变量保存 this
const self = this;
this.showFriends = function() {
this.friends.forEach(function(friend) {
console.log(self.name + ' is friends with ' + friend);
// 使用 self 而非 this,因为这里的 this 在非严格模式下指向全局
});
};
}
const profile = new UserProfile();
profile.showFriends();
// "Bruce is friends with Alice"
// "Bruce is friends with Bob"
🔥 常见面试题目
(一)this 关键字的值是如何决定的?请说明其判断顺序。
判断 this 的值有固定优先顺序,从高到低依次为:
- •如果是箭头函数,this 保持定义时外部环境的 this 值,不受其他规则影响
- •如果使用 new 调用函数,this 指向新创建的对象实例
- •如果使用 call、apply、bind 等方法,this 指向指定的对象
- •如果函数作为对象的方法调用,this 指向该对象
- •如果是普通函数调用,在非严格模式下 this 指向全局对象,严格模式下为 undefined
// 示例分析 this 绑定顺序
// 1. 箭头函数优先级最高
const arrowFn = () => { console.log(this) };
arrowFn.call({ name: 'test' }); // 全局对象,call 无法改变箭头函数的 this
// 2. new 绑定高于显式绑定
function Person(name) {
this.name = name;
console.log(this);
}
const boundPerson = Person.bind({ type: 'bound object' });
const person = new boundPerson('Bruce'); // { name: 'Bruce' },忽略了 bind 的对象
// 3. 显式绑定高于隐含绑定
const obj = {
name: 'object',
display() { console.log(this.name); }
};
obj.display.call({ name: 'call object' }); // "call object", call overrides method's this
// 4. 隐含绑定高于预设绑定
obj.display(); // "object",this 指向 obj
// 5. 预设绑定优先级最低
function standalone() { console.log(this); }
standalone(); // 全局对象或 undefined (严格模式)
(二)call、apply 和 bind 的差异与使用场景?
这三个方法都能显式指定函数执行时的 this 值,但在使用方式和行为上有明显差异:
方法 | 执行方式 | 参数传递 | 常见使用场景 |
---|---|---|---|
call | 立即执行函数 | 逐一传入参数 | 借用其他对象方法、继承时调用父类构造函数 |
apply | 立即执行函数 | 参数以数组形式传入 | 处理有大量参数的场景、数学计算(如求数组最大值) |
bind | 返回新函数,不立即执行 | 可预设部分参数 | 事件处理函数、回调函数中保持 this 上下文 |
// call 实例:借用数组方法处理类数组对象
function formatArgs() {
// arguments 是类数组对象,没有 join 方法
return Array.prototype.join.call(arguments, ' - ');
}
console.log(formatArgs('a', 'b', 'c')); // "a - b - c"
// apply 实例:求数组最大值
const numbers = [5, 3, 8, 1, 9, 4];
const max = Math.max.apply(null, numbers); // 等同于 Math.max(5, 3, 8, 1, 9, 4)
console.log(max); // 9
// bind 实例:保存 this 上下文于事件处理函数中
class Gallery {
constructor() {
this.images = ['img1.jpg', 'img2.jpg', 'img3.jpg'];
this.currentIndex = 0;
// 使用 bind 固定 this
document.getElementById('next').addEventListener(
'click',
this.showNext.bind(this)
);
}
showNext() {
this.currentIndex = (this.currentIndex + 1) % this.images.length;
console.log('Now showing: ' + this.images[this.currentIndex]);
}
}
(三)箭头函数与一般函数在 this 绑定上有什么不同?
箭头函数与一般函数在 this 绑定上有以下主要差异:
- •箭头函数没有自己的 this,它会继承定义时外部环境的 this 值
- •箭头函数的 this 绑定是固定的,无法通过 call、apply、bind 等方法改变
- •一般函数的 this 值取决于调用方式,可以通过各种方法改变
// 箭头函数与一般函数的 this 绑定差异
const obj = {
name: 'object',
regularMethod() {
console.log('Regular:', this.name);
setTimeout(function() {
console.log('Regular in timeout:', this.name); // undefined
}, 100);
},
arrowMethod() {
console.log('Arrow:', this.name);
setTimeout(() => {
console.log('Arrow in timeout:', this.name); // 'object'
}, 100);
}
};
obj.regularMethod();
obj.arrowMethod();
// 无法改变箭头函数的 this
const arrowFn = () => console.log(this);
arrowFn.call({ name: 'test' }); // 仍然指向全局对象
// 一般函数可以改变 this
function regularFn() { console.log(this.name); }
regularFn.call({ name: 'test' }); // 'test'
(五)判断下列代码中 this 的指向:
// 题目 1:基本绑定
const obj = {
name: 'object',
method() {
console.log(this.name);
}
};
obj.method(); // 'object' (隐含绑定)
// 题目 2:方法赋值
const method = obj.method;
method(); // undefined (预设绑定)
// 题目 3:回调函数
setTimeout(obj.method, 100); // undefined (预设绑定)
// 题目 4:箭头函数
const arrowObj = {
name: 'arrow',
method() {
setTimeout(() => {
console.log(this.name);
}, 100);
}
};
arrowObj.method(); // 'arrow' (箭头函数继承外部 this)
// 题目 5:构造函数
function Person(name) {
this.name = name;
this.sayHi = function() {
console.log('Hi, ' + this.name);
};
}
const person = new Person('Bruce');
person.sayHi(); // 'Hi, Bruce' (new 绑定)
// 解答:
// 1. obj.method() 中的 this 指向 obj,因为是通过对象调用方法(隐含绑定)
// 2. method() 中的 this 指向全局对象(非严格模式)或 undefined(严格模式),因为是直接调用(预设绑定)
// 3. setTimeout 中的 this 指向全局对象,因为回调函数是直接调用(预设绑定)
// 4. 箭头函数中的 this 指向 arrowObj,因为箭头函数继承了外部环境的 this(词法绑定)
// 5. person.sayHi() 中的 this 指向 person 实例,因为是通过 new 创建的对象调用方法(new 绑定)