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

JavaScript对象方法中的this绑定

2021-03-275.5k 阅读

理解 JavaScript 中的 this 概念

在 JavaScript 里,this 是一个特殊的关键字,它的值在函数执行时才确定,并且依赖于函数的调用方式。它不是在函数定义时就固定的,这一点和许多其他编程语言有很大不同。

this 通常指向一个对象,而这个对象被称为执行上下文对象。执行上下文是 JavaScript 代码执行时的一个抽象概念,它包含了函数执行所需的各种信息,比如作用域、变量对象等。this 就是执行上下文的一个属性。

例如,在全局作用域中,this 指向全局对象。在浏览器环境下,全局对象是 window;在 Node.js 环境下,全局对象是 global

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

函数调用模式与 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 对象的方法被调用,所以 this 指向 person 对象,从而可以正确输出 person 对象的 name 属性。

构造函数调用模式与 this 绑定

  1. 创建实例与 this 构造函数是一种特殊的函数,用于创建对象实例。当使用 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.'

Animal 构造函数中,this 指向新创建的 dog 对象(当通过 new 调用时)。所以 this.name 实际上是给新创建的对象添加了 name 属性,this.speak 给新对象添加了 speak 方法。

  1. 构造函数的原型与 this 构造函数的原型对象上的方法也会涉及到 this 绑定。当通过实例调用原型上的方法时,this 同样指向该实例。
function Bird(name) {
    this.name = name;
}
Bird.prototype.fly = function() {
    console.log(this.name +'is flying.');
};
const sparrow = new Bird('Sparrow');
sparrow.fly(); // 输出 'Sparrow is flying.'

在这个例子中,fly 方法定义在 Bird 构造函数的原型上。当 sparrow 实例调用 fly 方法时,this 指向 sparrow,因此可以正确输出实例的 name 属性。

call、apply 和 bind 方法与 this 绑定

  1. call 方法 call 方法允许显式地设置函数内部 this 的值。它的第一个参数就是要绑定到 this 的对象,后面的参数则作为函数的参数依次传递。
function greet(message) {
    console.log(message + ', I am'+ this.name);
}
const person1 = { name: 'Alice' };
const person2 = { name: 'Bob' };
greet.call(person1, 'Hello'); // 输出 'Hello, I am Alice'
greet.call(person2, 'Hi'); // 输出 'Hi, I am Bob'

在上述代码中,通过 call 方法,我们分别将 greet 函数内部的 this 绑定到 person1person2 对象上,从而实现不同的输出。

  1. apply 方法 apply 方法和 call 方法类似,也是用于设置函数内部 this 的值。不同之处在于,apply 方法的第二个参数是一个数组,数组中的元素作为函数的参数。
function sum(a, b) {
    return a + b;
}
const numbers = [3, 5];
const result = sum.apply(null, numbers);
console.log(result); // 输出 8

这里使用 apply 方法调用 sum 函数,并将 numbers 数组作为参数传递。第一个参数 null 表示在非严格模式下,this 指向全局对象;在严格模式下,thisnull

  1. bind 方法 bind 方法会创建一个新的函数,新函数内部的 this 被永久地绑定到 bind 方法的第一个参数上。
function multiply(a, b) {
    return a * b;
}
const boundMultiply = multiply.bind(null, 5);
const result2 = boundMultiply(3);
console.log(result2); // 输出 15

在这个例子中,bind 方法创建了一个新函数 boundMultiply,并将 multiply 函数内部的 this 绑定到 null,同时固定了第一个参数为 5。当调用 boundMultiply 时,只需要传递第二个参数 3,就可以得到结果。

箭头函数与 this 绑定

  1. 箭头函数的 this 特性 箭头函数与普通函数在 this 绑定上有很大的不同。箭头函数没有自己的 this,它的 this 继承自外层作用域。
const person = {
    name: 'Eve',
    getGreeting: function() {
        return () => {
            console.log('Hello, I am'+ this.name);
        };
    }
};
const greeting = person.getGreeting();
greeting(); // 输出 'Hello, I am Eve'

在上述代码中,箭头函数 () => {... } 没有自己的 this,它的 this 指向外层函数 getGreeting 中的 this,而 getGreeting 作为 person 对象的方法被调用,所以这里的 this 指向 person 对象。

  1. 与普通函数 this 绑定的对比 如果将上述代码中的箭头函数换成普通函数,情况就会不同。
const person3 = {
    name: 'Frank',
    getGreeting: function() {
        return function() {
            console.log('Hello, I am'+ this.name);
        };
    }
};
const greeting2 = person3.getGreeting();
greeting2(); // 在非严格模式下,输出 'Hello, I am undefined',因为此时 this 指向全局对象,全局对象没有 name 属性

在这个例子中,普通函数有自己独立的 this 绑定。当 getGreeting 返回的普通函数被调用时,它是作为独立函数调用的(不是作为 person3 对象的方法),所以在非严格模式下,this 指向全局对象,而全局对象没有 name 属性,从而输出 undefined

DOM 事件处理中的 this 绑定

  1. 传统事件绑定方式 在传统的 DOM 事件绑定方式中,当事件处理函数被调用时,this 指向触发事件的 DOM 元素。
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>DOM this binding</title>
</head>

<body>
    <button id="myButton">Click me</button>
    <script>
        const button = document.getElementById('myButton');
        button.onclick = function() {
            console.log(this.textContent);
        };
    </script>
</body>

</html>

当用户点击按钮时,事件处理函数中的 this 指向按钮元素,所以可以通过 this.textContent 获取按钮的文本内容。

  1. addEventListener 方法 使用 addEventListener 方法绑定事件时,情况类似,this 同样指向触发事件的 DOM 元素。
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>DOM this binding with addEventListener</title>
</head>

<body>
    <input type="text" id="inputField">
    <script>
        const input = document.getElementById('inputField');
        input.addEventListener('input', function() {
            console.log(this.value);
        });
    </script>
</body>

</html>

在这个例子中,当输入框有输入时,事件处理函数中的 this 指向输入框元素,因此可以获取输入框的 value 值。

嵌套函数中的 this 绑定问题与解决方法

  1. 问题表现 在一个函数内部定义的嵌套函数,其 this 绑定往往不是我们期望的那样。
const outerObject = {
    name: 'Outer',
    printName: function() {
        function innerFunction() {
            console.log(this.name);
        }
        innerFunction();
    }
};
outerObject.printName(); // 在非严格模式下,输出 undefined,因为 innerFunction 作为独立函数调用,this 指向全局对象,全局对象没有 name 属性

在上述代码中,innerFunctionprintName 内部的嵌套函数。当 innerFunction 被调用时,它是作为独立函数调用的,所以在非严格模式下,this 指向全局对象,而全局对象没有 name 属性,导致输出 undefined

  1. 解决方法 - 使用 that 或 self 一种常见的解决方法是在外部函数中保存 this 的值,通常使用 thatself 变量。
const outerObject2 = {
    name: 'Outer2',
    printName: function() {
        const that = this;
        function innerFunction() {
            console.log(that.name);
        }
        innerFunction();
    }
};
outerObject2.printName(); // 输出 'Outer2'

这里通过 const that = this; 将外部函数 printName 中的 this 值保存到 that 变量中。在 innerFunction 中使用 that 来访问外部对象的属性,从而得到正确的结果。

  1. 解决方法 - 使用箭头函数 由于箭头函数没有自己的 this,它继承外层作用域的 this,所以可以用箭头函数来解决嵌套函数的 this 绑定问题。
const outerObject3 = {
    name: 'Outer3',
    printName: function() {
        const innerFunction = () => {
            console.log(this.name);
        };
        innerFunction();
    }
};
outerObject3.printName(); // 输出 'Outer3'

在这个例子中,箭头函数 innerFunctionthis 继承自 printName 函数的 this,因此可以正确输出 outerObject3name 属性。

模块中的 this 绑定

  1. ES6 模块 在 ES6 模块中,this 的值为 undefined。这是因为 ES6 模块是在严格模式下执行的,并且模块本身并不是一个函数调用,所以没有默认的 this 绑定。
// module.js
console.log(this); // 输出 undefined
  1. CommonJS 模块 在 Node.js 中使用的 CommonJS 模块中,exportsmodule.exports 是用于导出模块内容的方式。在模块内部,this 指向 module.exports
// commonjsModule.js
console.log(this === module.exports); // 输出 true

这种 this 绑定方式在 CommonJS 模块的开发中有时会被用到,比如可以通过 this 来添加模块的属性和方法。

深入理解 this 绑定的原理

  1. 执行上下文栈 JavaScript 引擎在执行代码时,会维护一个执行上下文栈。当代码执行进入一个函数时,会创建一个新的执行上下文并将其压入栈中。当函数执行完毕,该执行上下文会从栈中弹出。this 的值是在执行上下文创建时确定的。

  2. 函数调用的内部机制 当一个函数被调用时,JavaScript 引擎会创建一个包含 this 绑定、作用域链等信息的执行上下文。对于不同的调用模式(如独立函数调用、方法调用、构造函数调用等),this 的绑定规则不同。这些规则是基于函数调用时的调用点(即函数是如何被调用的)来确定的。

例如,在方法调用模式下,函数的调用点明确了它是作为某个对象的方法被调用,所以 this 就指向该对象。而在独立函数调用模式下,调用点没有明确的对象关联,所以在非严格模式下 this 指向全局对象,在严格模式下 thisundefined

  1. 原型链与 this 在涉及原型链的情况下,this 的绑定依然遵循上述的基本规则。当通过实例调用原型上的方法时,this 指向该实例,这是因为方法的调用点是通过实例发起的,所以 this 绑定到实例对象,而不是原型对象本身。

总结常见的 this 绑定错误及避免方法

  1. 常见错误 - 混淆不同调用模式下的 this 绑定 例如,在将一个原本作为对象方法的函数提取出来单独调用时,可能会忘记 this 绑定的变化。
const obj = {
    value: 42,
    printValue: function() {
        console.log(this.value);
    }
};
const func = obj.printValue;
func(); // 在非严格模式下,输出 undefined,因为 func 作为独立函数调用,this 指向全局对象,全局对象没有 value 属性

避免这种错误的方法是要清楚地知道函数的调用模式和 this 的绑定规则。如果需要在不同的调用场景下保持 this 的一致性,可以使用 bind 方法提前绑定 this

const obj2 = {
    value: 42,
    printValue: function() {
        console.log(this.value);
    }
};
const func2 = obj2.printValue.bind(obj2);
func2(); // 输出 42
  1. 常见错误 - 箭头函数与普通函数 this 绑定混淆 如前文所述,箭头函数和普通函数在 this 绑定上有很大差异。在需要使用普通函数的 this 绑定特性时使用了箭头函数,或者反之,都可能导致错误。
const person4 = {
    name: 'Grace',
    createGreeting: function() {
        return function() {
            return 'Hello, I am'+ this.name;
        };
    }
};
const greeting3 = person4.createGreeting();
console.log(greeting3()); // 在非严格模式下,输出 'Hello, I am undefined',因为内部普通函数的 this 指向全局对象

// 错误地将普通函数换成箭头函数
const person5 = {
    name: 'Hank',
    createGreeting: function() {
        return () => {
            return 'Hello, I am'+ this.name;
        };
    }
};
const greeting4 = person5.createGreeting();
console.log(greeting4()); // 输出 'Hello, I am Hank',但如果期望内部函数有自己独立的 this 绑定,这就不符合预期了

避免这种错误的关键是要牢记箭头函数和普通函数 this 绑定的不同规则。在编写代码时,根据实际需求选择合适的函数类型。

  1. 常见错误 - DOM 事件处理函数中 this 绑定错误 在 DOM 事件处理中,如果使用了错误的函数类型或者对 this 的指向有错误的预期,也会导致问题。
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>DOM this binding error</title>
</head>

<body>
    <button id="wrongButton">Click me</button>
    <script>
        const wrongButton = document.getElementById('wrongButton');
        wrongButton.addEventListener('click', () => {
            console.log(this); // 在浏览器环境下,输出 window,因为箭头函数的 this 继承自外层作用域,这里外层是全局作用域
        });
    </script>
</body>

</html>

如果期望在事件处理函数中通过 this 访问按钮元素,应该使用普通函数。

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>DOM this binding correct</title>
</head>

<body>
    <button id="correctButton">Click me</button>
    <script>
        const correctButton = document.getElementById('correctButton');
        correctButton.addEventListener('click', function() {
            console.log(this.textContent);
        });
    </script>
</body>

</html>

通过以上对各种常见错误的分析和避免方法的介绍,开发者可以更加准确地使用 this 绑定,编写出更健壮的 JavaScript 代码。

通过深入理解 JavaScript 对象方法中的 this 绑定,我们可以更好地掌控函数的行为,避免常见的错误,编写出更高效、可靠的代码。无论是在简单的脚本编写,还是复杂的大型项目开发中,对 this 绑定的准确把握都是至关重要的。