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

JavaScript中的this指向问题全解析

2023-06-204.0k 阅读

一、全局环境中的this指向

在JavaScript的全局作用域中,this指向全局对象。在浏览器环境中,全局对象是window;在Node.js环境中,全局对象是global。以下是示例代码:

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

在上述代码中,直接在全局作用域中访问this,它与window对象是等同的。这意味着在全局作用域中定义的变量、函数等,实际上都是window对象的属性和方法。例如:

var globalVar = 'I am a global variable';
function globalFunction() {
    console.log('I am a global function');
}
console.log(window.globalVar); // 输出 'I am a global variable'
window.globalFunction(); // 输出 'I am a global function'

这里定义的globalVar变量和globalFunction函数,都可以通过window对象来访问,因为它们实际上是window对象的属性和方法,这进一步说明了全局作用域中this指向window

二、函数调用中的this指向

  1. 独立函数调用 当一个函数作为独立函数被调用时,在非严格模式下,this指向全局对象。例如:
function independentFunction() {
    console.log(this);
}
independentFunction(); // 在浏览器非严格模式下输出 window 对象

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

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

在上述代码中,sayHello函数是person对象的方法,当person.sayHello()被调用时,this指向person对象,所以能够正确输出person对象的name属性。 3. 嵌套函数中的this指向 当存在嵌套函数时,内部函数的this指向可能会让人困惑。例如:

var outerObject = {
    name: 'Outer',
    innerFunction: function() {
        function nestedFunction() {
            console.log(this.name);
        }
        nestedFunction();
    }
};
outerObject.innerFunction(); // 在非严格模式下输出 undefined(期望输出 Outer 但未实现)

在上述代码中,内部函数nestedFunction作为独立函数调用,在非严格模式下this指向全局对象,而全局对象并没有name属性,所以输出undefined。如果要在嵌套函数中访问外部对象的this,可以通过将外部的this赋值给一个变量,然后在内部函数中使用该变量。例如:

var outerObject = {
    name: 'Outer',
    innerFunction: function() {
        var self = this;
        function nestedFunction() {
            console.log(self.name);
        }
        nestedFunction();
    }
};
outerObject.innerFunction(); // 输出 'Outer'

在ES6的箭头函数中,处理方式有所不同,箭头函数没有自己的this,它的this继承自外层作用域,这在后面会详细介绍。

三、构造函数中的this指向

  1. 构造函数基础与this指向 构造函数是用于创建对象的特殊函数。当使用new关键字调用构造函数时,会创建一个新的对象,并且构造函数内部的this指向这个新创建的对象。例如:
function Person(name, age) {
    this.name = name;
    this.age = age;
    this.sayInfo = function() {
        console.log('Name:'+ this.name + ', Age:'+ this.age);
    };
}
var john = new Person('John', 30);
john.sayInfo(); // 输出 'Name: John, Age: 30'

在上述代码中,通过new Person('John', 30)创建了一个新的Person对象john。在Person构造函数内部,this指向新创建的john对象,所以可以给john对象添加nameage属性以及sayInfo方法。 2. 构造函数返回值对this指向的影响 通常情况下,构造函数不需要显式返回值,new操作符会返回新创建的对象。但如果构造函数显式返回一个对象,那么new操作符返回的就是这个显式返回的对象,而不是构造函数内部this指向的对象。例如:

function ReturningConstructor() {
    this.value = 'Original';
    return {
        value: 'Returned'
    };
}
var result = new ReturningConstructor();
console.log(result.value); // 输出 'Returned'

在上述代码中,ReturningConstructor构造函数显式返回了一个对象,所以new操作符返回的是这个显式返回的对象,而不是this指向的原本要创建的对象。如果构造函数返回的不是一个对象(如返回一个基本类型值),则new操作符仍然返回构造函数内部this指向的对象。例如:

function ReturningPrimitiveConstructor() {
    this.value = 'Original';
    return 'Returned';
}
var primitiveResult = new ReturningPrimitiveConstructor();
console.log(primitiveResult.value); // 输出 'Original'

这里构造函数返回了一个字符串基本类型值,new操作符还是返回了构造函数内部this指向的对象。

四、箭头函数中的this指向

  1. 箭头函数this指向特点 箭头函数没有自己的this,它的this继承自外层作用域。这与普通函数有很大的不同。例如:
var person = {
    name: 'John',
    getArrowFunction: function() {
        return () => {
            console.log(this.name);
        };
    }
};
var arrowFunction = person.getArrowFunction();
arrowFunction(); // 输出 'John'

在上述代码中,箭头函数() => { console.log(this.name); }没有自己的this,它的this继承自外层函数getArrowFunctionthis,而getArrowFunction作为person对象的方法,其this指向person对象,所以箭头函数能够正确输出person对象的name属性。 2. 与普通函数对比理解箭头函数this指向 对比普通函数,在相同场景下行为不同。例如:

var person = {
    name: 'John',
    getRegularFunction: function() {
        return function() {
            console.log(this.name);
        };
    }
};
var regularFunction = person.getRegularFunction();
regularFunction(); // 在非严格模式下输出 undefined

这里普通函数function() { console.log(this.name); }有自己独立的this,当作为独立函数调用时,在非严格模式下this指向全局对象,全局对象没有name属性,所以输出undefined。而箭头函数由于没有自己的this,依赖外层作用域的this,所以表现不同。 3. 多层嵌套箭头函数的this指向 即使存在多层嵌套的箭头函数,其this仍然继承自最外层非箭头函数作用域的this。例如:

var outerObject = {
    name: 'Outer',
    innerFunction: function() {
        return () => {
            return () => {
                console.log(this.name);
            };
        };
    }
};
var innerArrow = outerObject.innerFunction();
var nestedInnerArrow = innerArrow();
nestedInnerArrow(); // 输出 'Outer'

在上述代码中,无论箭头函数嵌套多少层,它们的this都继承自最外层innerFunction函数的this,而innerFunction作为outerObject的方法,this指向outerObject,所以最终能够正确输出outerObjectname属性。

五、call、apply和bind方法对this指向的影响

  1. call方法 call方法用于调用一个函数,并指定函数内部this的指向。其语法为function.call(thisArg, arg1, arg2,...),其中thisArg是要指定的this值,后面的参数是函数的参数。例如:
function greet(message) {
    console.log(message + ', I am'+ this.name);
}
var person1 = {
    name: 'John'
};
var person2 = {
    name: 'Jane'
};
greet.call(person1, 'Hello'); // 输出 'Hello, I am John'
greet.call(person2, 'Hi'); // 输出 'Hi, I am Jane'

在上述代码中,通过greet.call(person1, 'Hello'),将greet函数内部的this指定为person1对象,所以能够输出person1对象的name属性。call方法可以在不同对象上复用同一个函数,通过改变this的指向来适应不同对象的上下文。 2. apply方法 apply方法与call方法类似,也是用于调用函数并指定this的指向,但它接收参数的方式不同。apply方法的语法为function.apply(thisArg, [argsArray]),其中thisArg是要指定的this值,argsArray是一个包含函数参数的数组。例如:

function sum(a, b) {
    return a + b;
}
var numbers = [5, 3];
var result = sum.apply(null, numbers);
console.log(result); // 输出 8

在上述代码中,sum.apply(null, numbers)sum函数内部的this指定为null(在非严格模式下,如果指定为nullundefinedthis会指向全局对象),并通过数组numbers传递参数。apply方法在需要将数组作为函数参数传递时非常有用。 3. bind方法 bind方法用于创建一个新的函数,这个新函数的this被绑定到指定的值。其语法为function.bind(thisArg, arg1, arg2,...),其中thisArg是要绑定的this值,后面的参数是新函数的预设参数。例如:

function multiply(a, b) {
    return a * b;
}
var double = multiply.bind(null, 2);
var result = double(5);
console.log(result); // 输出 10

在上述代码中,multiply.bind(null, 2)创建了一个新函数double,并将this绑定为null(在非严格模式下指向全局对象),同时预设了第一个参数为2。当调用double(5)时,实际上相当于调用multiply(2, 5),所以返回10bind方法常用于需要固定函数this指向,并可能预设部分参数的场景。

六、事件处理函数中的this指向

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

<head>
    <meta charset="UTF - 8">
    <title>Event this example</title>
</head>

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

</html>

在上述代码中,当点击按钮时,onclick事件处理函数中的this指向按钮元素,所以能够输出按钮的文本内容。 2. addEventListener方法中事件处理函数的this指向 使用addEventListener方法添加事件处理函数时,this同样指向触发事件的DOM元素。例如:

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>addEventListener this example</title>
</head>

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

</html>

在上述代码中,通过addEventListener添加的点击事件处理函数,this也指向按钮元素,所以能输出按钮的文本内容。但如果使用箭头函数作为事件处理函数,情况有所不同。例如:

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>Arrow event this example</title>
</head>

<body>
    <button id="arrowButton">Click for arrow</button>
    <script>
        var arrowButton = document.getElementById('arrowButton');
        arrowButton.addEventListener('click', () => {
            console.log(this);
        });
    </script>
</body>

</html>

这里箭头函数没有自己的this,其this继承自外层作用域,在这个例子中外层作用域可能是全局作用域(取决于代码位置),所以this可能指向全局对象,而不是按钮元素。如果在箭头函数中需要访问按钮元素,可以通过保存外层作用域的this或者使用参数传递。例如:

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>Arrow event this fix example</title>
</head>

<body>
    <button id="fixedArrowButton">Click to fix</button>
    <script>
        var fixedArrowButton = document.getElementById('fixedArrowButton');
        fixedArrowButton.addEventListener('click', function() {
            var self = this;
            return () => {
                console.log(self.textContent);
            };
        }());
    </script>
</body>

</html>

在上述代码中,通过在普通函数中保存thisself,然后在箭头函数中使用self,就可以正确访问按钮元素的文本内容。

七、eval中的this指向

  1. 非严格模式下eval的this指向 在非严格模式下,eval中的this指向全局对象。例如:
var globalValue = 'Global';
function evalFunction() {
    eval('console.log(this.globalValue)');
}
evalFunction(); // 输出 'Global'

在上述代码中,eval内部的this指向全局对象,所以能够访问全局变量globalValue。 2. 严格模式下eval的this指向 在严格模式下,eval中的this指向调用eval的上下文。例如:

function strictEvalFunction() {
    'use strict';
    var localValue = 'Local';
    var obj = {
        value: 'Object'
    };
    eval.call(obj, 'console.log(this.value)');
    eval('console.log(this)'); // 这里的 this 指向调用 eval 的上下文,在严格模式下不会自动指向全局对象
}
strictEvalFunction();

在上述代码中,通过eval.call(obj, 'console.log(this.value)'),将eval内部的this指向obj对象,所以能输出obj对象的value属性。而在eval('console.log(this)')中,this指向调用eval的上下文(这里由于严格模式,不是全局对象)。

八、解决this指向问题的最佳实践

  1. 使用箭头函数替代普通函数 当需要确保函数内部this与外层作用域this一致时,优先使用箭头函数。例如在对象方法内部需要定义内部函数来访问外部对象的属性时,箭头函数可以避免this指向问题。
var dataObject = {
    data: [1, 2, 3],
    processData: function() {
        return this.data.map((value) => {
            return value * 2;
        });
    }
};
var result = dataObject.processData();
console.log(result); // 输出 [2, 4, 6]

在上述代码中,箭头函数(value) => { return value * 2; }this继承自processData函数的this,所以能够正确访问dataObjectdata属性。 2. 使用bind方法预先绑定this 在需要复用函数并固定this指向时,使用bind方法。例如,在事件处理函数中,如果需要确保this指向特定对象,可以使用bind方法。

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>Bind in event example</title>
</head>

<body>
    <button id="bindButton">Click for bind</button>
    <script>
        var bindObject = {
            message: 'Bound message'
        };
        function boundFunction() {
            console.log(this.message);
        }
        var button = document.getElementById('bindButton');
        button.addEventListener('click', boundFunction.bind(bindObject));
    </script>
</body>

</html>

在上述代码中,通过boundFunction.bind(bindObject)boundFunction函数的this绑定为bindObject,这样在按钮点击事件处理中,this就会指向bindObject,从而正确输出bindObjectmessage属性。 3. 合理使用闭包保存this 在一些不适合使用箭头函数的场景(如需要函数有自己的this时),可以通过闭包保存外层作用域的this。例如在普通函数内部的嵌套函数中:

var containerObject = {
    name: 'Container',
    innerFunction: function() {
        var self = this;
        function nestedFunction() {
            console.log(self.name);
        }
        nestedFunction();
    }
};
containerObject.innerFunction(); // 输出 'Container'

在上述代码中,通过var self = this将外层函数的this保存为self,在嵌套函数中使用self就可以访问到外层对象的属性。

通过深入理解JavaScript中this的指向规则,并结合这些最佳实践,可以有效避免this指向问题带来的错误,编写出更健壮和可维护的JavaScript代码。同时,在不同的场景下,如全局环境、函数调用、构造函数、箭头函数、事件处理等,灵活运用this的指向特性,能够充分发挥JavaScript语言的优势,实现复杂的业务逻辑。