MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript中的this关键字详解

2023-04-172.1k 阅读

一、this 关键字的基本概念

在 JavaScript 中,this 关键字是一个特殊的内置对象,它的值在函数调用时动态绑定,取决于函数的调用方式。this 的值并不是在函数定义时确定的,而是在函数执行时确定的。这使得 this 的行为有时会让人困惑,但也赋予了 JavaScript 极大的灵活性。

(一)全局作用域中的 this

在全局作用域中(即在任何函数外部),this 指向全局对象。在浏览器环境中,全局对象是 window;在 Node.js 环境中,全局对象是 global

console.log(this === window); // 在浏览器中输出 true
console.log(this); // 在浏览器中输出 window 对象

这里通过判断 this 是否严格等于 window,直观地展示了在浏览器全局作用域下 this 的指向。

(二)函数调用中的 this

  1. 普通函数调用 当函数作为普通函数调用时(不是作为对象的方法调用),this 指向全局对象。
    function sayHello() {
        console.log(this);
    }
    sayHello(); // 在浏览器中输出 window 对象
    
    这里定义了一个简单的 sayHello 函数,直接调用该函数,this 指向了全局的 window 对象。
  2. 对象方法调用 当函数作为对象的方法被调用时,this 指向调用该方法的对象。
    const person = {
        name: 'Alice',
        sayHello: function() {
            console.log(`Hello, I'm ${this.name}`);
        }
    };
    person.sayHello(); // 输出 Hello, I'm Alice
    
    在这个例子中,sayHello 函数是 person 对象的方法,当调用 person.sayHello() 时,this 指向 person 对象,所以能够正确输出 person 对象的 name 属性值。

二、this 绑定规则深入剖析

(一)默认绑定

默认绑定是最基础的 this 绑定规则,适用于普通函数调用场景。当函数独立调用(非对象方法调用、无其他绑定规则影响)时,this 指向全局对象。这一规则在严格模式和非严格模式下略有不同。

  1. 非严格模式 在非严格模式下,默认绑定使得 this 指向全局对象。
    function printThis() {
        console.log(this);
    }
    printThis(); // 在浏览器非严格模式下输出 window 对象
    
  2. 严格模式 在严格模式下,函数中的 this 不会默认指向全局对象,而是 undefined
    'use strict';
    function printThis() {
        console.log(this);
    }
    printThis(); // 输出 undefined
    
    这里通过在脚本开头添加 'use strict'; 启用严格模式,展示了在严格模式下普通函数调用时 this 的指向为 undefined

(二)隐式绑定

隐式绑定发生在函数作为对象的方法被调用时。此时,this 指向调用该方法的对象。关键在于函数调用的上下文,即函数前面的对象。

const car = {
    brand: 'Toyota',
    describe: function() {
        console.log(`This is a ${this.brand} car.`);
    }
};
car.describe(); // 输出 This is a Toyota car.

在上述代码中,describe 函数作为 car 对象的方法调用,this 隐式绑定到 car 对象,从而可以正确访问 car 对象的 brand 属性。

(三)显式绑定

  1. call 方法 call 方法允许显式地设置函数调用时 this 的值。它的第一个参数就是要绑定的 this 值,后面可以跟多个参数作为函数的实参。
    function greet(message) {
        console.log(`${message}, I'm ${this.name}`);
    }
    const person1 = { name: 'Bob' };
    greet.call(person1, 'Hello'); // 输出 Hello, I'm Bob
    
    这里通过 call 方法将 greet 函数的 this 绑定到 person1 对象,并传递了 'Hello' 作为参数。
  2. apply 方法 apply 方法与 call 方法类似,也是用于显式绑定 this,但它的第二个参数是一个数组,数组中的元素作为函数的实参。
    function sum(a, b) {
        return a + b;
    }
    const numbers = [2, 3];
    const result = sum.apply(null, numbers);
    console.log(result); // 输出 5
    
    在这个例子中,apply 方法的第一个参数 null 表示 this 不绑定到任何对象(在非严格模式下会指向全局对象),第二个参数 numbers 数组中的元素作为 sum 函数的参数。
  3. bind 方法 bind 方法会创建一个新的函数,新函数的 this 被绑定到指定的值。与 callapply 不同,bind 方法不会立即调用函数,而是返回一个新的绑定了 this 的函数。
    function multiply(a, b) {
        return a * b;
    }
    const multiplyByTwo = multiply.bind(null, 2);
    const result = multiplyByTwo(5);
    console.log(result); // 输出 10
    
    这里通过 bind 方法将 multiply 函数的 this 绑定为 null(同样在非严格模式下指向全局对象),并固定第一个参数为 2,返回一个新函数 multiplyByTwo,调用 multiplyByTwo 并传入参数 5 得到最终结果。

(四)new 绑定

当使用 new 关键字调用函数(构造函数)时,会发生 new 绑定。新创建的对象会成为构造函数中 this 的值。

function Animal(name) {
    this.name = name;
    this.speak = function() {
        console.log(`${this.name} makes a sound.`);
    };
}
const dog = new Animal('Buddy');
dog.speak(); // 输出 Buddy makes a sound.

在这个例子中,通过 new 关键字调用 Animal 构造函数,新创建的 dog 对象成为了 Animal 函数内部 this 的值,从而可以为 dog 对象添加 name 属性和 speak 方法。

三、this 绑定优先级

(一)优先级顺序

  1. new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定 当多种绑定规则可能同时作用于一个函数调用时,new 绑定具有最高优先级,其次是显式绑定(callapplybind),然后是隐式绑定,最后是默认绑定。
    function greet() {
        console.log(`Hello, I'm ${this.name}`);
    }
    const person = { name: 'Eve' };
    const newGreet = greet.bind(person);
    const newPerson = new newGreet();
    console.log(newPerson.name); // 输出 Eve
    
    在这个例子中,bind 方法进行了显式绑定,new 关键字又进行了 new 绑定。由于 new 绑定优先级高于显式绑定,最终 this 指向新创建的 newPerson 对象,并且该对象具有 name 属性值为 Eve
  2. 箭头函数的特殊情况 箭头函数不具有自己的 this,它的 this 继承自外层作用域(词法作用域)。这意味着箭头函数的 this 不会受到上述绑定规则的影响。
    const obj = {
        name: 'John',
        getThis: function() {
            return () => this;
        }
    };
    const result = obj.getThis()();
    console.log(result === obj); // 输出 true
    
    getThis 方法中,返回了一个箭头函数。这个箭头函数的 this 继承自 getThis 方法的 this,即 obj 对象,所以最终 result 严格等于 obj

四、this 关键字在实际开发中的常见问题与解决方案

(一)丢失隐式绑定

  1. 问题表现 当将对象的方法赋值给一个变量,然后通过该变量调用函数时,会丢失隐式绑定,this 会指向全局对象(在非严格模式下)或 undefined(在严格模式下)。
    const obj = {
        name: 'Alice',
        sayHello: function() {
            console.log(`Hello, I'm ${this.name}`);
        }
    };
    const func = obj.sayHello;
    func(); // 在非严格模式下输出 Hello, I'm undefined(因为 this 指向全局对象,无 name 属性)
    
    在上述代码中,将 obj.sayHello 赋值给 func,然后调用 func,此时函数调用不再是作为 obj 的方法调用,从而丢失了隐式绑定。
  2. 解决方案
    • 使用 bind 方法:可以在赋值时使用 bind 方法将 this 绑定到正确的对象。
    const obj = {
        name: 'Alice',
        sayHello: function() {
            console.log(`Hello, I'm ${this.name}`);
        }
    };
    const func = obj.sayHello.bind(obj);
    func(); // 输出 Hello, I'm Alice
    
    • 使用箭头函数:如果合适,将方法改为箭头函数,利用箭头函数继承外层作用域 this 的特性。
    const obj = {
        name: 'Alice',
        sayHello: () => {
            console.log(`Hello, I'm ${this.name}`);
        }
    };
    const func = obj.sayHello;
    func(); // 这里 this 指向全局对象,在浏览器中输出 Hello, I'm [window 对象的 name 属性值,通常为 undefined]
    // 但如果外层作用域的 this 是正确的对象,这种方式可行
    
    不过需要注意的是,使用箭头函数作为对象方法时,要确保外层作用域的 this 是期望的值,否则可能会出现意外结果。

(二)箭头函数与 this 的误解

  1. 问题表现 由于箭头函数没有自己的 this,而是继承外层作用域的 this,有时会因为对作用域理解不清而导致 this 指向错误。
    function outerFunction() {
        this.name = 'Bob';
        const innerFunction = () => {
            console.log(this.name);
        };
        innerFunction();
    }
    outerFunction(); // 输出 Bob
    const outer = new outerFunction();
    // 如果在严格模式下,这里的 new outerFunction() 可能会有不同行为,因为 this 指向 undefined 时给 name 赋值会报错
    
    在这个例子中,如果认为箭头函数 innerFunctionthis 会指向函数内部创建的某个局部对象,就会产生误解。实际上,它继承自 outerFunctionthis
  2. 解决方案
    • 明确作用域:在编写代码时,清楚地知道箭头函数的 this 继承规则,确保外层作用域的 this 是期望的值。
    • 使用普通函数替代:如果需要函数有自己独立的 this 绑定,使用普通函数而不是箭头函数。
    function outerFunction() {
        this.name = 'Bob';
        function innerFunction() {
            console.log(this.name);
        }
        innerFunction.call(this); // 输出 Bob
    }
    outerFunction();
    
    这里使用普通函数 innerFunction,并通过 call 方法显式地将 this 绑定到 outerFunctionthis,以达到预期效果。

五、this 关键字在不同场景下的应用

(一)事件处理中的 this

在 DOM 事件处理中,this 通常指向触发事件的 DOM 元素。

const button = document.createElement('button');
button.textContent = 'Click me';
button.addEventListener('click', function() {
    this.style.backgroundColor = 'red';
});
document.body.appendChild(button);

在这个例子中,当按钮被点击时,事件处理函数中的 this 指向按钮元素,从而可以直接修改按钮的样式。

(二)模块中的 this

在 JavaScript 模块中,this 的行为与普通脚本略有不同。模块的顶层 this 通常是 undefined

// module.js
console.log(this); // 输出 undefined

这是因为模块具有自己独立的作用域,与全局作用域隔离,this 不再指向全局对象。

(三)类中的 this

在 ES6 类中,this 的行为基于方法的调用方式。在类的实例方法中,this 指向类的实例。

class Person {
    constructor(name) {
        this.name = name;
    }
    sayHello() {
        console.log(`Hello, I'm ${this.name}`);
    }
}
const person = new Person('Charlie');
person.sayHello(); // 输出 Hello, I'm Charlie

Person 类的 sayHello 方法中,this 指向 person 实例,从而可以访问实例的 name 属性。

六、总结 this 关键字的特性与要点

  1. 动态绑定this 的值在函数调用时确定,而非定义时,这是理解 this 行为的关键。
  2. 绑定规则:掌握默认绑定、隐式绑定、显式绑定和 new 绑定这四种规则及其优先级。箭头函数不遵循这些常规绑定规则,而是继承外层作用域的 this
  3. 实际问题:注意在实际开发中可能出现的丢失隐式绑定、对箭头函数 this 误解等问题,并掌握相应的解决方案。
  4. 应用场景:了解 this 在事件处理、模块、类等不同场景下的具体应用和行为差异。

通过深入理解和实践 this 关键字的这些特性和要点,开发者能够更好地编写健壮、可读的 JavaScript 代码,避免因 this 指向错误而导致的各种问题。