【前端面试小册】JS-第13节:面试必会 - this 专题
一、this 的概念
1.1 定义
this 是 JavaScript 语言的关键字之一,this 既不指向函数自身也不指向函数的词法作用域。
核心理解:this 是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。
类比理解:this 就像一个"指向标",它的指向不是固定的,而是根据函数的调用方式动态决定的。就像同一个"指向标"在不同的人手里,指向的方向也不同。
1.2 关键点
this的绑定发生在函数调用时,而不是函数定义时this的指向完全取决于函数的调用方式this不是静态的,是动态的
二、this 绑定规则
2.1 默认绑定
函数直接调用,不带修饰(call、apply 等)会有如下规则:
- 非严格模式:浏览器环境
this指向window,Node 环境this指向Global - 严格模式:
this指向undefined
Demo 1:基本默认绑定
var name = '成都巴菲特'
var obj = {
name: '知识星球:前端职场圈',
log: function() {
console.log(this.name)
}
}
var log = obj.log
log() // 浏览器中输出: "成都巴菲特"
分析:
log直接调用,没有任何修饰,所以走默认绑定规则this指向window,而var定义的全局变量会默认挂在window上this.name = window.name输出:成都巴菲特
注意:如果这里直接在 Node 环境下输出的是 undefined,因为 this 指向 Global,而 var 定义的变量不会挂在 Global 上。
Demo 2:setTimeout 中的默认绑定
var name = '成都巴菲特';
var person = {
name: '公众号:前端面试资源',
log: log
}
function log() {
setTimeout(function() {
console.log('Hi,', this.name);
})
}
person.log();
setTimeout(function() {
person.log();
}, 200);
// 输出结果
// Hi, 成都巴菲特
// Hi, 成都巴菲特
分析:
- 函数的参数是函数,比如
setTimeout,参数(函数)中的this在非严格模式指向全局对象(浏览器:window,Node 环境:Global) setTimeout的回调函数中的this指向全局对象
2.2 隐式绑定
函数调用的时候是否在上下文中调用,或者说是否某个对象调用函数。
Demo 1:普通情况
var name = '成都巴菲特';
var person = {
name: '公众号:前端面试资源',
log: function() {
console.log('Hi,', this.name);
}
}
person.log(); // 公众号:前端面试资源
分析:log 作为对象属性调用的,隐式绑定会把函数中 this 指向绑定到这个上下文对象 person。
Demo 2:嵌套调用情况
在多层嵌套调用函数时,如:对象.对象.函数,this 指向的是最后一层对象。
function log() {
console.log('Hi', this.name);
}
var child = {
name: '成都巴菲特',
log: log
}
var father = {
name: 'koala',
child: child
}
father.child.log(); // Hi 成都巴菲特
分析:this 指向的是最后一层对象 child,而不是 father。
2.3 显式绑定
通过函数 call、apply、bind 可以修改函数 this 的指向。
知识点回顾
call、apply 与 bind 区别:
call、apply:改变this指向同时执行函数bind:改变this指向,但是返回函数,而不是执行函数
call 与 apply 区别:
func.call(this, arg1, arg2) // call 用法
func.apply(this, [arg1, arg2]) // apply 用法
// 第一个参数都是 this 所绑定的对象
// call 接受的参数列表,apply 接受参数数组
Demo 1:基本显式绑定
var person = {
name: "成都巴菲特",
};
function log(name, job) {
this.name = name;
this.job = job;
}
log.call(person, '程序员', '前端开发');
console.log(person.name); // '程序员'
log.apply(person, ['程序员', '前端开发']);
console.log(person.job); // '前端开发'
Demo 2:显式绑定成功
function sayHi() {
console.log('hi,', this.name);
}
var person = {
name: '成都巴菲特',
sayHi: sayHi
}
var name = '程序员';
var hi = person.sayHi;
hi.call(person); // hi, 成都巴菲特
Demo 3:显式绑定失效?
function sayHi() {
console.log('hi,', this.name);
}
var person = {
name: '成都巴菲特',
sayHi: sayHi
}
var name = '程序员';
var hi = function(fn) {
fn();
}
hi.call(person, person.sayHi); // hi, 程序员
分析:
demo2明明用了call来改变this指向是显式绑定,为什么this.name不是person里面的name值'成都巴菲特',而是全局变量中的'程序员'?
原因:
- 因为执行:
hi.call(person, person.sayHi)的时候是把hi的this绑定在person上 - 但是
fn()的调用没有改变this指向,所以走默认规则
var hi = function(fn) {
fn(); // 直接调用,没有改变 this 指向
}
// 等价于
var hi = function(fn = sayHi) {
sayHi(); // 直接调用,走默认绑定
}
Demo 4:解决显式绑定失效
function sayHi() {
console.log('hi,', this.name);
}
var person = {
name: '成都巴菲特',
sayHi: sayHi
}
var name = '程序员';
var hi = function(fn) {
fn.call(this); // 将 fn 的 this 绑定到 hi 的 this(即 person)
}
hi.call(person, person.sayHi); // hi, 成都巴菲特
分析:因为我们改变的是 hi 的 this 指向,并没有改变 fn 的 this 指向,那么把 fn 的 this 指向绑定到 hi 上,其实就是相当于绑定在了 hi 指向的 person。
2.4 new 绑定
使用 new 会执行如下流程:
- 创建一个空对象
- 将空对象的
__proto__指向原对象的prototype - 执行构造函数中的代码
- 返回这个新对象
Demo
function Person(name) {
this.name = name;
}
var p = new Person('成都巴菲特');
console.log(p.name); // 成都巴菲特
知识点回顾:实现一个 new
function myNew() {
// 创建的新对象
let obj = {};
// 第一个参数是构造函数
let [constructor, ...args] = [...arguments];
// 执行[[原型]]连接;obj 是 constructor 的实例
obj.__proto__ = constructor.prototype;
// 执行构造函数,将属性或方法添加到创建的空对象上
let result = constructor.apply(obj, args);
if (result && (typeof (result) == "object" || typeof (result) == "function")) {
// 如果构造函数执行的结果返回的是一个对象,那么返回这个对象
return result;
}
// 如果构造函数返回的不是对象,那么返回我们创建对象
return obj;
}
2.5 绑定优先级
有的时候函数可能会应用多种规则,这个时候按照他们优先级来判断:
优先级规则:new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定
// 示例
function log() {
console.log(this.name);
}
var obj1 = { name: 'obj1', log: log };
var obj2 = { name: 'obj2', log: log };
// 显式绑定 > 隐式绑定
log.call(obj1); // obj1(显式绑定)
obj1.log(); // obj1(隐式绑定)
// new 绑定 > 显式绑定
var boundLog = log.bind(obj1);
var instance = new boundLog(); // undefined(new 绑定覆盖了显式绑定)
2.6 绑定例外
apply、call、bind 的 this 新指向对象如果是 null、undefined 会失效,实际走默认绑定。
var obj = {
name: '成都巴菲特'
}
var name = '程序员';
function log() {
console.log(this.name);
}
log.call(null); // 程序员(null 失效,走默认绑定)
三、箭头函数的 this
3.1 箭头函数特点
- 没有自己的
this,继承外层代码块this - 不能作为构造函数,所以不能使用
new - 没有
arguments对象 - 不能使用
yield,所以不能作为 Generator 函数 - 没有自己的
this,无法使用call、apply、bind去显式改变this指向
3.2 箭头函数 this 示例
var name = '成都巴菲特'
let name1 = 'name1'
let obj = {
name: '程序员',
log: () => {
console.log(this.name)
},
log1: () => {
console.log(this.name1)
},
log2: function() {
return () => console.log(this.name)
}
}
obj.log() // 成都巴菲特(箭头函数继承外层 this,即 window)
obj.log1() // undefined(let 声明的变量不在 window 上)
const log2 = obj.log2()
log2() // 程序员(箭头函数继承 log2 的 this,即 obj)
3.3 箭头函数 this 不是静态的
注意:箭头函数是继承外层 this,但不是静态的。
var obj = {
sayHi: function() {
return function() {
console.log(this);
return () => {
console.log(this);
}
}
}
}
let sayHi = obj.sayHi();
let fun1 = sayHi(); // 输出 window
fun1(); // 输出 window
let fun2 = sayHi.bind(obj)(); // 输出 obj
fun2(); // 输出 obj
分析:
let fun2 = function() {
// 这里应用默认规则 this(假设是 this1)指向 window
console.log(this);
return () => {
// 这里的 this 假设是(this2),因为箭头函数没有自己的 this,
// 所以 this2 默认继承外面的 this1 也就是 window
console.log(this);
}
}
然而:let fun2 = sayHi.bind(obj)() 改变了 this 的指向,让 this1 指向了 obj,而 this2 继承 this1 所以也指向了 obj。
四、面试题
4.1 题目 1:let vs var 在箭头函数中的 this
let name = '知识星球:前端职场圈'
let obj = {
name: '微信公众号:前端面试资源',
test: () => {
console.log(this.name)
}
}
obj.test() // 输出是什么?
答案:undefined
解析:
- 使用
let声明的变量,调用函数的this指向window - 而
let声明的变量挂在Script上,所以window.name为undefined
4.2 题目 2:var vs let 在箭头函数中的 this
var name = '知识星球:前端职场圈'
let obj = {
name: '微信公众号:前端面试资源',
test: () => {
console.log(this.name)
}
}
obj.test() // 输出是什么?
答案:知识星球:前端职场圈
解析:
- 使用
var声明的变量,调用函数的this指向window - 而
var声明的变量挂在window上,所以window.name有值
知识点回顾:let 与 var 在全局中声明变量区别?
let:在全局中创建的变量存在于Script中,它与window(Global)平级var:在全局中创建的变量存在于window(Global)中
4.3 题目 3:复杂箭头函数 this
var obj = {
log1: function() {
console.log(this);
return () => {
console.log(this);
}
},
log2: function() {
return function() {
console.log(this);
return () => {
console.log(this);
}
}
},
log3: () => {
console.log(this);
}
}
let log1 = obj.log1();
log1();
let log2 = obj.log2();
let fun1 = log2();
fun1();
obj.log3();
答案:
let log1 = obj.log1(); // 输出 obj 对象
log1(); // 输出 obj 对象
let log2 = obj.log2();
let fun1 = log2(); // 输出 window
fun1(); // 输出 window
obj.log3(); // 输出 window
分析:
obj.log1():隐式绑定规则,this绑定在obj上,所以输出objlog1():这一步执行的就是箭头函数,箭头函数继承上一个代码块的this,刚刚我们得出上一层的this是obj,显然这里的this就是objlog2():这个时候相当于在全局重新定义了一个log2的函数,这个时候this执行的是默认绑定,this指向的是全局对象windowfun1():这里fun1是箭头函数,this是继承于外层代码块的this,此时外层this指向的是window,因此这儿的输出结果是windowobj.log3():obj.log3也是箭头函数,this继承外层obj的this,但是代码块obj中是不存在this的,只能往上找,就找到了全局的this,指向的是window
五、this 绑定规则总结
5.1 绑定规则流程图
graph TD
A[函数调用] --> B{是否使用 new?}
B -->|是| C[new 绑定: this = 新对象]
B -->|否| D{是否使用 call/apply/bind?}
D -->|是| E[显式绑定: this = 指定对象]
D -->|否| F{是否作为对象方法调用?}
F -->|是| G[隐式绑定: this = 调用对象]
F -->|否| H{是否箭头函数?}
H -->|是| I[继承外层 this]
H -->|否| J[默认绑定: this = window/undefined]
5.2 优先级总结
- new 绑定:
new调用时,this指向新创建的对象 - 显式绑定:
call、apply、bind调用时,this指向指定对象 - 隐式绑定:作为对象方法调用时,
this指向调用对象 - 默认绑定:直接调用时,
this指向全局对象(非严格模式)或undefined(严格模式) - 箭头函数:继承外层代码块的
this
六、面试要点总结
核心知识点
- this 的绑定时机:函数调用时,不是定义时
- 绑定规则:new > 显式 > 隐式 > 默认
- 箭头函数:没有自己的
this,继承外层 - 特殊情况:
call/apply/bind传入null/undefined会失效
常见面试题
Q1: this 的指向规则有哪些?
答:有四种绑定规则:new 绑定、显式绑定(call/apply/bind)、隐式绑定(对象方法调用)、默认绑定(直接调用)。优先级:new > 显式 > 隐式 > 默认。
Q2: 箭头函数的 this 指向什么?
答:箭头函数没有自己的 this,继承外层代码块的 this。无法通过 call、apply、bind 改变。
Q3: 如何改变 this 的指向?
答:
- 使用
call、apply、bind(显式绑定) - 使用
new(new 绑定) - 作为对象方法调用(隐式绑定)
- 箭头函数无法改变(继承外层)
实战建议
- ✅ 理解
this的绑定规则和优先级 - ✅ 注意箭头函数和普通函数的
this区别 - ✅ 理解
call、apply、bind的使用场景 - ✅ 注意
let和var在全局作用域的区别对this的影响
每天更新3-4节,持续更新中... 目标:50天学完,上岸银行总行!
查看18道真题和解析
