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

JavaScript函数中的this绑定规则

2024-07-126.7k 阅读

JavaScript函数中的this绑定规则

一、全局作用域中的this

在JavaScript的全局作用域中,this的指向取决于运行环境。在浏览器环境下,全局作用域中的this指向window对象。例如:

console.log(this === window); // true
function logThis() {
    console.log(this === window);
}
logThis(); // true

上述代码中,在全局作用域中直接使用this,它指向window对象。在定义的logThis函数内部,没有特别的绑定规则改变this指向时,this同样指向window对象。

在Node.js环境下,情况稍有不同。在Node.js的模块顶层作用域中,this并不指向全局对象global。例如:

console.log(this === global); // false

这里的this实际上指向的是当前模块的exports对象的一个引用。不过,这种情况只在模块顶层作用域有效,在函数内部依然遵循其他的this绑定规则。

二、函数调用模式下的this

  1. 独立函数调用 当一个函数作为独立函数被调用时,也就是没有通过对象的属性来调用,在非严格模式下,this指向全局对象。例如:
function sayHello() {
    console.log(this);
}
sayHello(); // 在浏览器环境下输出window对象

在严格模式下,情况有所不同。在严格模式下,独立函数调用时this的值为undefined。例如:

function strictSayHello() {
    'use strict';
    console.log(this);
}
strictSayHello(); // 输出undefined
  1. 方法调用 当函数作为对象的方法被调用时,this指向调用该方法的对象。例如:
const person = {
    name: 'John',
    sayName: function () {
        console.log(this.name);
    }
};
person.sayName(); // 输出John

在上述代码中,sayName函数是person对象的一个方法。当通过person.sayName()调用时,this指向person对象,所以能够正确输出person对象的name属性值。

三、构造函数调用模式下的this

  1. 构造函数基础 当使用new关键字调用一个函数时,该函数被当作构造函数。在构造函数内部,this指向新创建的对象实例。例如:
function Person(name) {
    this.name = name;
    this.sayName = function () {
        console.log(this.name);
    };
}
const john = new Person('John');
john.sayName(); // 输出John

在上述代码中,通过new Person('John')创建了一个新的Person实例john。在Person构造函数内部,this指向新创建的john对象实例,因此可以给this添加name属性和sayName方法。

  1. 构造函数返回值对this的影响 构造函数的返回值会影响new操作符最终返回的结果,进而影响this的使用。如果构造函数返回一个对象,那么new操作符返回的就是这个对象,而不是构造函数内部的this所指向的对象。例如:
function Person() {
    this.name = 'default';
    return {name: 'override'};
}
const person = new Person();
console.log(person.name); // 输出override

在上述代码中,Person构造函数内部给this设置了namedefault,但由于返回了一个新的对象{name: 'override'}new操作符返回的是这个返回的对象,所以person.name输出override

如果构造函数返回的不是一个对象(比如返回一个基本类型值,如return 1;),那么new操作符会忽略这个返回值,依然返回构造函数内部this所指向的对象。例如:

function Person() {
    this.name = 'default';
    return 1;
}
const person = new Person();
console.log(person.name); // 输出default

四、call、apply和bind方法对this的绑定

  1. call方法 call方法允许显式地设置函数内部this的指向。它的第一个参数就是要绑定的this值,后面可以跟一系列参数,这些参数会作为函数的参数依次传入。例如:
function sayHello() {
    console.log('Hello, ' + this.name);
}
const person1 = {name: 'John'};
const person2 = {name: 'Jane'};
sayHello.call(person1); // 输出Hello, John
sayHello.call(person2); // 输出Hello, Jane

在上述代码中,通过sayHello.call(person1)sayHello函数内部的this绑定到person1对象,所以输出Hello, John;通过sayHello.call(person2)this绑定到person2对象,输出Hello, Jane

  1. apply方法 apply方法和call方法类似,也是用于显式绑定this。不同之处在于,apply方法的第二个参数是一个数组,数组中的元素会作为函数的参数依次传入。例如:
function sum(a, b) {
    return a + b;
}
const numbers = [1, 2];
const result = sum.apply(null, numbers);
console.log(result); // 输出3

在上述代码中,sum.apply(null, numbers)sum函数内部的this绑定为null(在非严格模式下,nullundefined作为callapply的第一个参数时,this会指向全局对象),并将numbers数组中的元素作为sum函数的参数传入,所以得到结果3。

  1. bind方法 bind方法也用于绑定this,但它和callapply不同的是,bind方法不会立即调用函数,而是返回一个新的函数,这个新函数内部的this已经被绑定。例如:
function sayHello() {
    console.log('Hello, ' + this.name);
}
const person = {name: 'John'};
const boundSayHello = sayHello.bind(person);
boundSayHello(); // 输出Hello, John

在上述代码中,sayHello.bind(person)返回一个新的函数boundSayHello,在这个新函数内部,this已经被绑定到person对象,所以调用boundSayHello()输出Hello, John

五、箭头函数中的this

  1. 箭头函数this的特点 箭头函数没有自己独立的this绑定,它的this继承自外层作用域。例如:
const person = {
    name: 'John',
    sayHello: function () {
        const innerFunction = () => {
            console.log(this.name);
        };
        innerFunction();
    }
};
person.sayHello(); // 输出John

在上述代码中,innerFunction是一个箭头函数,它没有自己的this,所以它的this继承自外层函数sayHellothis,而sayHello是作为person对象的方法被调用,this指向person对象,因此输出John

  1. 与传统函数this的对比 箭头函数的this绑定规则与传统函数有很大区别。传统函数在不同的调用模式下,this会有不同的指向。而箭头函数无论在什么情况下,它的this始终继承自外层作用域。例如:
function TraditionalFunction() {
    this.name = 'Traditional';
    function innerFunction() {
        console.log(this.name);
    }
    innerFunction();
}
const traditional = new TraditionalFunction(); // 输出undefined

function ArrowFunction() {
    this.name = 'Arrow';
    const innerFunction = () => {
        console.log(this.name);
    };
    innerFunction();
}
const arrow = new ArrowFunction(); // 输出Arrow

在上述代码中,TraditionalFunction内部的innerFunction是一个传统函数,它在独立调用时,在非严格模式下this指向全局对象,所以输出undefined。而ArrowFunction内部的innerFunction是箭头函数,它的this继承自外层的ArrowFunction构造函数内部的this,所以输出Arrow

  1. 箭头函数在事件处理中的应用 在事件处理中,使用箭头函数可以避免this指向混乱的问题。例如:
<button id="myButton">Click me</button>
<script>
    const button = document.getElementById('myButton');
    const person = {
        name: 'John',
        handleClick: function () {
            button.addEventListener('click', () => {
                console.log('Hello, ' + this.name);
            });
        }
    };
    person.handleClick();
</script>

在上述代码中,如果addEventListener的回调函数使用传统函数,在点击按钮时,this会指向按钮元素,而不是person对象。但使用箭头函数,它的this继承自handleClick函数的this,所以能正确输出Hello, John

六、嵌套函数中的this

  1. 传统函数嵌套 在传统函数嵌套的情况下,内层函数的this指向会遵循其自身的调用模式,而不是继承自外层函数。例如:
function outerFunction() {
    this.name = 'Outer';
    function innerFunction() {
        console.log(this.name);
    }
    innerFunction();
}
const outer = new outerFunction(); // 输出undefined

在上述代码中,innerFunction是一个独立调用的传统函数,在非严格模式下,this指向全局对象,所以输出undefined

  1. 解决传统嵌套函数this问题的方法 为了解决传统嵌套函数中this指向的问题,可以使用var self = this或者const that = this的方式来保存外层函数的this。例如:
function outerFunction() {
    const that = this;
    this.name = 'Outer';
    function innerFunction() {
        console.log(that.name);
    }
    innerFunction();
}
const outer = new outerFunction(); // 输出Outer

在上述代码中,通过const that = this保存了外层函数outerFunctionthis,在内层函数innerFunction中使用that来访问外层函数的this所指向的对象,从而正确输出Outer

另外,也可以使用箭头函数来解决这个问题。因为箭头函数没有自己独立的this,会继承外层函数的this。例如:

function outerFunction() {
    this.name = 'Outer';
    const innerFunction = () => {
        console.log(this.name);
    };
    innerFunction();
}
const outer = new outerFunction(); // 输出Outer

在上述代码中,innerFunction是箭头函数,它的this继承自outerFunction,所以能正确输出Outer

七、事件处理函数中的this

  1. HTML事件处理属性中的this 在HTML标签的事件处理属性中,this指向触发事件的DOM元素。例如:
<button onclick="console.log(this)">Click me</button>

当点击按钮时,控制台会输出按钮元素,因为this指向了触发click事件的按钮。

  1. addEventListener中的this 在使用addEventListener添加事件监听器时,默认情况下,事件处理函数中的this也指向触发事件的DOM元素。例如:
<button id="myButton">Click me</button>
<script>
    const button = document.getElementById('myButton');
    button.addEventListener('click', function () {
        console.log(this);
    });
</script>

上述代码中,点击按钮时,控制台会输出按钮元素。如果想改变this的指向,可以使用callapplybind方法。例如:

<button id="myButton">Click me</button>
<script>
    const person = {name: 'John'};
    const button = document.getElementById('myButton');
    button.addEventListener('click', function () {
        console.log(this.name);
    }.bind(person));
</script>

在上述代码中,通过.bind(person)将事件处理函数内部的this绑定到person对象,这样点击按钮时,虽然是按钮触发事件,但this指向person对象。如果使用箭头函数作为事件处理函数,情况又有所不同。因为箭头函数没有自己的this,它的this继承自外层作用域。例如:

<button id="myButton">Click me</button>
<script>
    const person = {name: 'John'};
    const button = document.getElementById('myButton');
    button.addEventListener('click', () => {
        console.log(this.name);
    });
</script>

在上述代码中,箭头函数的this继承自外层作用域,这里外层作用域是全局作用域(在浏览器环境下this指向window对象,window对象没有name属性),所以不会输出John。如果想在箭头函数中访问person对象的name属性,可以这样做:

<button id="myButton">Click me</button>
<script>
    const person = {name: 'John'};
    const button = document.getElementById('myButton');
    button.addEventListener('click', function () {
        const self = this;
        const arrowFunction = () => {
            console.log(person.name);
        };
        arrowFunction();
    });
</script>

八、定时器函数中的this

  1. setTimeout和setInterval的基本this指向setTimeoutsetInterval的回调函数中,this的指向取决于函数的定义方式。如果使用传统函数作为回调函数,在非严格模式下,this指向全局对象。例如:
function Timer() {
    this.name = 'Timer';
    setTimeout(function () {
        console.log(this.name);
    }, 1000);
}
const timer = new Timer(); // 输出undefined

在上述代码中,setTimeout的回调函数是传统函数,在非严格模式下,它独立调用时this指向全局对象,全局对象没有name属性,所以输出undefined

  1. 解决定时器函数this问题的方法 为了解决这个问题,可以使用bind方法来绑定this。例如:
function Timer() {
    this.name = 'Timer';
    setTimeout(function () {
        console.log(this.name);
    }.bind(this), 1000);
}
const timer = new Timer(); // 输出Timer

在上述代码中,通过.bind(this)setTimeout回调函数内部的this绑定到Timer构造函数内部的this,所以能正确输出Timer

也可以使用箭头函数作为setTimeoutsetInterval的回调函数。因为箭头函数没有自己独立的this,会继承外层作用域的this。例如:

function Timer() {
    this.name = 'Timer';
    setTimeout(() => {
        console.log(this.name);
    }, 1000);
}
const timer = new Timer(); // 输出Timer

在上述代码中,箭头函数的this继承自Timer构造函数内部的this,所以能正确输出Timer

九、类方法中的this

  1. 类的基本定义与this指向 在ES6类中,类的方法内部的this指向类的实例。例如:
class Person {
    constructor(name) {
        this.name = name;
    }
    sayName() {
        console.log(this.name);
    }
}
const john = new Person('John');
john.sayName(); // 输出John

在上述代码中,sayName方法是Person类的一个实例方法,在sayName方法内部,this指向john这个Person类的实例,所以能正确输出John

  1. 类方法作为回调函数时的this问题 当类的方法作为回调函数传递给其他函数时,可能会出现this指向错误的问题。例如:
class Person {
    constructor(name) {
        this.name = name;
    }
    sayName() {
        console.log(this.name);
    }
}
const john = new Person('John');
setTimeout(john.sayName, 1000); // 输出undefined

在上述代码中,john.sayName作为回调函数传递给setTimeout,在setTimeout执行回调函数时,sayName函数是以独立函数的形式调用(非严格模式下this指向全局对象),所以输出undefined

为了解决这个问题,可以使用bind方法。例如:

class Person {
    constructor(name) {
        this.name = name;
    }
    sayName() {
        console.log(this.name);
    }
}
const john = new Person('John');
setTimeout(john.sayName.bind(john), 1000); // 输出John

在上述代码中,通过bind(john)sayName函数内部的this绑定到john实例,所以能正确输出John。也可以在类的构造函数中提前绑定this。例如:

class Person {
    constructor(name) {
        this.name = name;
        this.sayName = this.sayName.bind(this);
    }
    sayName() {
        console.log(this.name);
    }
}
const john = new Person('John');
setTimeout(john.sayName, 1000); // 输出John

在上述代码中,在构造函数中通过this.sayName = this.sayName.bind(this)提前绑定了this,这样无论sayName方法在何处作为回调函数被调用,this都能正确指向john实例。

十、总结this绑定规则的优先级

  1. 显示绑定(call、apply、bind)的优先级 显示绑定(通过callapplybind方法)的优先级最高。只要使用了这些方法来绑定this,函数内部的this就会按照指定的方式指向。例如:
function sayHello() {
    console.log('Hello, ' + this.name);
}
const person1 = {name: 'John'};
const person2 = {name: 'Jane'};
sayHello.call(person1); // 输出Hello, John
sayHello.bind(person2)(); // 输出Hello, Jane

在上述代码中,通过callbind方法明确指定了this的指向,所以函数内部的this按照指定的对象来解析。

  1. new关键字调用的优先级 new关键字调用构造函数时,构造函数内部的this指向新创建的对象实例,其优先级仅次于显示绑定。例如:
function Person(name) {
    this.name = name;
    this.sayName = function () {
        console.log(this.name);
    };
}
const john = new Person('John');
john.sayName(); // 输出John

在上述代码中,通过new关键字创建Person实例时,构造函数内部的this指向新创建的john对象实例。

  1. 方法调用的优先级 当函数作为对象的方法被调用时,this指向调用该方法的对象,其优先级低于new关键字调用和显示绑定。例如:
const person = {
    name: 'John',
    sayName: function () {
        console.log(this.name);
    }
};
person.sayName(); // 输出John

在上述代码中,sayName作为person对象的方法被调用,this指向person对象。

  1. 独立函数调用的优先级 独立函数调用(非严格模式下this指向全局对象,严格模式下thisundefined)的优先级最低。例如:
function sayHello() {
    console.log(this);
}
sayHello(); // 在非严格模式下输出全局对象(浏览器环境下为window),严格模式下输出undefined

在上述代码中,sayHello作为独立函数调用,遵循其自身的低优先级this绑定规则。

  1. 箭头函数的优先级 箭头函数没有自己独立的this绑定,它的this继承自外层作用域,并且不受上述其他规则的影响。它的优先级需要结合外层作用域的this绑定情况来确定。例如:
const person = {
    name: 'John',
    sayHello: function () {
        const innerFunction = () => {
            console.log(this.name);
        };
        innerFunction();
    }
};
person.sayHello(); // 输出John

在上述代码中,箭头函数innerFunctionthis继承自外层的sayHello函数的this,而sayHello函数是作为person对象的方法被调用,所以最终输出John

通过理解这些this绑定规则的优先级,可以在复杂的JavaScript代码中准确地确定函数内部this的指向,避免因this指向错误而导致的各种问题。同时,在实际编程中,应根据具体需求选择合适的方式来控制this的指向,以确保代码的正确性和可维护性。

在实际项目开发中,深入理解this绑定规则对于编写高质量的JavaScript代码至关重要。无论是小型的前端页面交互,还是大型的后端Node.js应用,this的正确使用都直接影响到代码的逻辑和功能。希望通过以上详细的介绍和丰富的代码示例,能帮助开发者更好地掌握JavaScript函数中this的绑定规则。