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

JavaScript上下文与this的区别

2022-03-096.3k 阅读

JavaScript上下文概述

在JavaScript中,上下文(Context)是一个非常重要的概念。简单来说,上下文定义了变量和函数的执行环境。每当JavaScript代码执行时,都会在特定的上下文中运行。上下文可以被看作是一个“环境包裹”,它包含了代码执行时可用的各种信息,比如变量、函数以及作用域等相关内容。

全局上下文

全局上下文是最外层的上下文。在浏览器环境中,全局上下文通常指的是window对象(在Node.js环境中则是global对象)。当JavaScript代码在全局作用域中运行时,它处于全局上下文当中。在全局上下文中定义的变量和函数,都成为了全局对象(windowglobal)的属性和方法。

// 在全局上下文中定义变量
var globalVariable = 'This is a global variable';
console.log(window.globalVariable); // 输出: This is a global variable

// 在全局上下文中定义函数
function globalFunction() {
    console.log('This is a global function');
}
window.globalFunction(); // 输出: This is a global function

在上面的代码中,globalVariableglobalFunction虽然直接定义在全局作用域,但实际上它们成为了window对象的属性和方法,可以通过window对象来访问。

函数上下文

当函数被调用时,就会创建一个新的函数上下文。函数上下文有自己的一套变量对象(VO),用于存储在函数内部定义的变量和函数声明。函数上下文的作用域链是由函数的[[Scope]]属性构建的,它决定了函数在执行时如何查找变量。

function myFunction() {
    var localVar = 'This is a local variable';
    console.log(localVar);
}
myFunction(); // 输出: This is a local variable
// console.log(localVar); // 这里会报错,localVar只在myFunction函数上下文内有效

在这个例子中,localVar变量定义在myFunction函数上下文内,只有在函数内部才能访问,在函数外部访问会导致错误,因为超出了其所在的函数上下文作用域。

块级上下文(ES6 引入)

在ES6之前,JavaScript并没有真正的块级作用域概念。但是在ES6中,通过letconst关键字引入了块级上下文。块级上下文可以由{}代码块来定义,在块级上下文中使用letconst声明的变量,其作用域仅限于该代码块内部。

{
    let blockVar = 'This is a block - level variable';
    console.log(blockVar);
}
// console.log(blockVar); // 这里会报错,blockVar只在块级上下文中有效

上述代码展示了块级上下文的特性,blockVar变量定义在{}块级上下文中,在块外部无法访问。

this关键字详解

this关键字在JavaScript中用于引用当前执行上下文的对象。然而,this的值并不是在编写代码时确定的,而是在函数被调用时确定的。它的值取决于函数的调用方式,这使得this在不同的调用场景下可能有不同的值。

全局上下文中的this

在全局上下文中(非严格模式下),this指向全局对象。在浏览器环境中,这意味着this指向window对象。

console.log(this === window); // 输出: true

function globalThisFunction() {
    console.log(this === window);
}
globalThisFunction(); // 输出: true

在严格模式下,全局上下文中的this值为undefined

'use strict';
console.log(this); // 输出: undefined

函数调用中的this

  1. 作为对象方法调用:当函数作为对象的方法被调用时,this指向调用该方法的对象。
let obj = {
    name: 'John',
    sayHello: function() {
        console.log('Hello, my name is'+ this.name);
    }
};
obj.sayHello(); // 输出: Hello, my name is John

在上述代码中,sayHello函数作为obj对象的方法被调用,所以函数内部的this指向obj对象,从而能够正确输出对象的name属性。

  1. 独立函数调用:当函数独立调用(不是作为对象的方法)时,在非严格模式下,this指向全局对象(浏览器中是window)。
function independentFunction() {
    console.log(this === window);
}
independentFunction(); // 输出: true

在严格模式下,独立函数调用时this的值为undefined

'use strict';
function strictIndependentFunction() {
    console.log(this);
}
strictIndependentFunction(); // 输出: undefined
  1. 使用call、apply和bind方法:这三个方法允许我们显式地设置函数内部this的值。
    • call方法call方法接受一个this值和一系列参数,它会立即调用函数,并将this设置为传入的第一个参数。
let person1 = {
    name: 'Alice',
    sayHello: function(greeting) {
        console.log(greeting + ', my name is'+ this.name);
    }
};
let person2 = {
    name: 'Bob'
};

person1.sayHello.call(person2, 'Hi'); // 输出: Hi, my name is Bob

在上述代码中,person1.sayHello.call(person2, 'Hi')通过call方法将sayHello函数内部的this指向person2,并传入参数Hi,所以最终输出是Bob的信息。

- **apply方法**:`apply`方法与`call`方法类似,不同之处在于它接受一个数组作为参数列表。
let person3 = {
    name: 'Charlie'
};
person1.sayHello.apply(person3, ['Hello']); // 输出: Hello, my name is Charlie

这里apply方法将sayHello函数内部的this指向person3,并通过数组['Hello']传递参数。

- **bind方法**:`bind`方法创建一个新的函数,这个新函数内部的`this`值被绑定为`bind`方法的第一个参数。与`call`和`apply`不同,`bind`方法不会立即调用函数,而是返回一个新的绑定了`this`的函数。
let person4 = {
    name: 'David'
};
let newFunction = person1.sayHello.bind(person4, 'Hey');
newFunction(); // 输出: Hey, my name is David

在上述代码中,bind方法创建了一个新函数newFunction,并将this绑定为person4,同时预设了参数'Hey',调用newFunction时就会按照绑定的this和预设参数执行。

箭头函数中的this

箭头函数是ES6引入的一种新的函数语法。箭头函数没有自己的this绑定,它的this值继承自外层作用域(词法作用域)。

let outerObj = {
    name: 'Outer',
    inner: function() {
        let arrowFunction = () => {
            console.log(this.name);
        };
        arrowFunction();
    }
};
outerObj.inner(); // 输出: Outer

在上述代码中,箭头函数arrowFunction没有自己的this,它的this值继承自包含它的inner函数的上下文,而inner函数作为outerObj的方法被调用,所以this指向outerObj,最终输出Outer

JavaScript上下文与this的区别

  1. 定义和本质区别:上下文主要定义了代码执行的环境,包括变量、函数以及作用域等相关信息。它是一个更为宽泛的概念,决定了代码在何处以及如何执行。而this关键字是用于在函数执行时引用当前执行上下文的对象,它的值在函数调用时动态确定,并且与函数的调用方式紧密相关。

  2. 作用范围不同:上下文的作用范围涵盖了变量的可访问性、函数的作用域等多个方面。例如,函数上下文决定了函数内部变量的作用范围,块级上下文限制了letconst声明变量的作用范围。而this主要用于在函数内部引用特定的对象,它并不直接决定变量的作用范围。

  3. 确定时机不同:上下文在代码执行前就已经确定。例如,全局上下文在脚本加载时就创建,函数上下文在函数调用时创建。而this的值是在函数调用时才确定的,并且根据函数的调用方式不同而变化。

  4. 对代码影响不同:上下文影响着变量的查找和作用域链的构建。例如,在函数上下文中,变量首先在函数的变量对象中查找,如果找不到再沿着作用域链向上查找。而this主要影响函数内部对对象属性的访问和操作。例如,在作为对象方法调用的函数中,this指向对象,函数可以通过this访问和修改对象的属性。

  5. 箭头函数的特殊情况:箭头函数没有自己的this绑定,它依赖于外层作用域的上下文来确定this的值。这与普通函数上下文和this的关系不同,普通函数在调用时会根据调用方式确定this,而箭头函数的this是静态的,取决于其定义时的外层上下文。

// 普通函数示例
let normalObj = {
    value: 10,
    increment: function() {
        this.value++;
        console.log(this.value);
    }
};
normalObj.increment(); // 输出: 11

// 箭头函数示例
let arrowObj = {
    value: 20,
    increment: () => {
        // 这里的this不是指向arrowObj,而是外层作用域(全局上下文,这里是window,非严格模式下)
        console.log(this.value);
    }
};
arrowObj.increment(); // 输出: undefined(假设全局上下文中没有定义value)

在上述代码中,普通函数increment作为normalObj的方法调用时,this指向normalObj,能够正确增加并输出对象的value属性。而箭头函数increment由于没有自己的this,其this指向外层作用域(全局上下文,这里非严格模式下是window),而全局上下文中没有定义value,所以输出undefined

  1. 在事件处理中的差异:在传统的事件处理函数中,this通常指向触发事件的DOM元素(在浏览器环境中)。
<button id="myButton">Click me</button>
<script>
    let myButton = document.getElementById('myButton');
    myButton.onclick = function() {
        console.log(this === myButton); // 输出: true
    };
</script>

然而,如果使用箭头函数作为事件处理函数,由于箭头函数没有自己的this,它的this继承自外层作用域,通常不是我们期望的触发事件的DOM元素。

<button id="arrowButton">Click me with arrow function</button>
<script>
    let arrowButton = document.getElementById('arrowButton');
    arrowButton.onclick = () => {
        console.log(this === arrowButton); // 输出: false(this指向外层作用域,非严格模式下是window)
    };
</script>

总结两者区别的实际应用场景

  1. 面向对象编程:在面向对象编程中,上下文和this的正确理解和使用至关重要。通过合理利用函数上下文和this的指向,可以实现对象的封装、继承和多态等特性。例如,在构造函数中,this指向新创建的对象实例,通过this可以为对象实例添加属性和方法。
function Person(name) {
    this.name = name;
    this.sayName = function() {
        console.log('My name is'+ this.name);
    };
}
let person = new Person('Eve');
person.sayName(); // 输出: My name is Eve

在这个例子中,构造函数Person的上下文用于创建新的对象实例,this指向新创建的person对象,通过this为对象添加name属性和sayName方法。

  1. 事件驱动编程:如前文所述,在事件处理中,需要注意普通函数和箭头函数作为事件处理函数时this指向的不同。根据实际需求选择合适的函数类型,以确保能够正确访问和操作与事件相关的对象。

  2. 模块化编程:在模块化编程中,模块的上下文定义了模块内部变量和函数的作用范围。而this在模块内部的使用相对较少,因为模块通常通过导出函数和对象来提供功能,这些导出的函数和对象在外部调用时会有各自独立的上下文和this指向。

避免混淆上下文与this的常见错误

  1. 理解箭头函数的this绑定:由于箭头函数没有自己的this绑定,很容易在需要this指向特定对象的场景中错误地使用箭头函数。例如,在对象的方法中,如果使用箭头函数,可能无法正确访问对象的属性。要牢记箭头函数的this继承自外层作用域,在对象方法中使用普通函数来确保this指向对象本身。

  2. 严格模式与非严格模式下this的差异:在严格模式和非严格模式下,this在全局上下文和独立函数调用中的值是不同的。在编写代码时,要明确代码运行的模式,避免因模式不同导致this值的意外变化。特别是在将代码从非严格模式转换为严格模式时,要仔细检查this相关的代码。

  3. 事件处理函数中的this:在使用事件处理函数时,要清楚普通函数和箭头函数作为事件处理函数时this指向的差异。如果需要在事件处理函数中访问触发事件的元素或相关对象,应使用普通函数作为事件处理函数,或者在箭头函数中通过其他方式获取所需对象。

通过深入理解JavaScript上下文与this的区别,开发人员能够编写出更健壮、更易于维护的代码,避免因this指向错误或上下文理解不当而导致的各种难以调试的问题。无论是在小型脚本还是大型应用程序开发中,正确把握这两个概念都是非常关键的。