【前端面试小册】JS-第13节:面试必会 - this 专题

一、this 的概念

1.1 定义

this 是 JavaScript 语言的关键字之一,this 既不指向函数自身也不指向函数的词法作用域。

核心理解this 是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。

类比理解this 就像一个"指向标",它的指向不是固定的,而是根据函数的调用方式动态决定的。就像同一个"指向标"在不同的人手里,指向的方向也不同。

1.2 关键点

  • this 的绑定发生在函数调用时,而不是函数定义时
  • this 的指向完全取决于函数的调用方式
  • this 不是静态的,是动态的

二、this 绑定规则

2.1 默认绑定

函数直接调用,不带修饰(callapply 等)会有如下规则:

  • 非严格模式:浏览器环境 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 显式绑定

通过函数 callapplybind 可以修改函数 this 的指向。

知识点回顾

call、apply 与 bind 区别

  • callapply:改变 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) 的时候是把 hithis 绑定在 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, 成都巴菲特

分析:因为我们改变的是 hithis 指向,并没有改变 fnthis 指向,那么把 fnthis 指向绑定到 hi 上,其实就是相当于绑定在了 hi 指向的 person

2.4 new 绑定

使用 new 会执行如下流程:

  1. 创建一个空对象
  2. 将空对象的 __proto__ 指向原对象的 prototype
  3. 执行构造函数中的代码
  4. 返回这个新对象

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 绑定例外

applycallbindthis 新指向对象如果是 nullundefined 会失效,实际走默认绑定。

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,无法使用 callapplybind 去显式改变 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.nameundefined

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 有值

知识点回顾letvar 在全局中声明变量区别?

  • let:在全局中创建的变量存在于 Script 中,它与 windowGlobal)平级
  • var:在全局中创建的变量存在于 windowGlobal)中

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

分析

  1. obj.log1():隐式绑定规则,this 绑定在 obj 上,所以输出 obj
  2. log1():这一步执行的就是箭头函数,箭头函数继承上一个代码块的 this,刚刚我们得出上一层的 thisobj,显然这里的 this 就是 obj
  3. log2():这个时候相当于在全局重新定义了一个 log2 的函数,这个时候 this 执行的是默认绑定,this 指向的是全局对象 window
  4. fun1():这里 fun1 是箭头函数,this 是继承于外层代码块的 this,此时外层 this 指向的是 window,因此这儿的输出结果是 window
  5. obj.log3()obj.log3 也是箭头函数,this 继承外层 objthis,但是代码块 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 优先级总结

  1. new 绑定new 调用时,this 指向新创建的对象
  2. 显式绑定callapplybind 调用时,this 指向指定对象
  3. 隐式绑定:作为对象方法调用时,this 指向调用对象
  4. 默认绑定:直接调用时,this 指向全局对象(非严格模式)或 undefined(严格模式)
  5. 箭头函数:继承外层代码块的 this

六、面试要点总结

核心知识点

  1. this 的绑定时机:函数调用时,不是定义时
  2. 绑定规则:new > 显式 > 隐式 > 默认
  3. 箭头函数:没有自己的 this,继承外层
  4. 特殊情况call/apply/bind 传入 null/undefined 会失效

常见面试题

Q1: this 的指向规则有哪些?

答:有四种绑定规则:new 绑定、显式绑定(call/apply/bind)、隐式绑定(对象方法调用)、默认绑定(直接调用)。优先级:new > 显式 > 隐式 > 默认。

Q2: 箭头函数的 this 指向什么?

答:箭头函数没有自己的 this,继承外层代码块的 this。无法通过 callapplybind 改变。

Q3: 如何改变 this 的指向?

答:

  1. 使用 callapplybind(显式绑定)
  2. 使用 new(new 绑定)
  3. 作为对象方法调用(隐式绑定)
  4. 箭头函数无法改变(继承外层)

实战建议

  • ✅ 理解 this 的绑定规则和优先级
  • ✅ 注意箭头函数和普通函数的 this 区别
  • ✅ 理解 callapplybind 的使用场景
  • ✅ 注意 letvar 在全局作用域的区别对 this 的影响
#前端面试##前端#
前端面试小册 文章被收录于专栏

每天更新3-4节,持续更新中... 目标:50天学完,上岸银行总行!

全部评论

相关推荐

评论
点赞
1
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务