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

JavaScript中的作用域链与执行上下文

2023-09-064.1k 阅读

JavaScript 作用域基础

在 JavaScript 中,作用域是变量、函数和对象的可访问范围。它决定了代码中标识符(变量名、函数名等)的查找规则。作用域分为全局作用域和局部作用域。

全局作用域

全局作用域是最外层的作用域,在浏览器环境中,全局作用域通常是 window 对象(在 Node.js 环境中是 global 对象)。在全局作用域中声明的变量和函数,在整个脚本中都可以访问。例如:

// 在全局作用域声明变量
var globalVar = 'I am global';

function globalFunction() {
    console.log(globalVar);
}

globalFunction(); // 输出: I am global
console.log(globalVar); // 输出: I am global

在上述代码中,globalVarglobalFunction 都在全局作用域中声明,因此在函数内部和全局代码中都能访问到 globalVar

局部作用域

局部作用域是在函数内部定义的作用域。在函数内部声明的变量和函数,只能在该函数内部访问。例如:

function localScopeFunction() {
    var localVar = 'I am local';
    console.log(localVar);
}

localScopeFunction(); // 输出: I am local
console.log(localVar); // 报错: localVar is not defined

localScopeFunction 函数内部声明的 localVar,只能在该函数内部访问,在函数外部访问会导致错误。

在 ES6 之前,JavaScript 只有函数作用域,不存在块级作用域。块级作用域是指由 {} 包裹的代码块,如 if 语句块、for 循环块等。在 ES6 引入 letconst 关键字后,JavaScript 才有了块级作用域。例如:

// 使用 var 声明变量,没有块级作用域
if (true) {
    var varInBlock = 'I am in block';
}
console.log(varInBlock); // 输出: I am in block

// 使用 let 声明变量,具有块级作用域
if (true) {
    let letInBlock = 'I am in block with let';
}
console.log(letInBlock); // 报错: letInBlock is not defined

上述代码展示了 varlet 在块级作用域上的区别,var 声明的变量会被提升到函数作用域顶部,而 let 声明的变量具有块级作用域,在块外部无法访问。

作用域链

当在 JavaScript 中查找一个变量时,会从当前作用域开始,沿着作用域链向上查找,直到找到该变量或者到达全局作用域。作用域链是由多个作用域对象组成的链表。

作用域链的形成

每个函数在创建时,都会创建一个作用域链。这个作用域链包含了函数所在的作用域(通常是定义函数的作用域)以及其上层作用域,直到全局作用域。例如:

var outerVar = 'I am outer';

function outerFunction() {
    var innerVar = 'I am inner';

    function innerFunction() {
        console.log(outerVar);
        console.log(innerVar);
    }

    innerFunction();
}

outerFunction(); 
// 输出: 
// I am outer
// I am inner

在上述代码中,innerFunction 创建时,其作用域链包含了 outerFunction 的作用域和全局作用域。当 innerFunction 查找 outerVarinnerVar 时,会先在自己的作用域中查找(没有找到 outerVar),然后沿着作用域链到 outerFunction 的作用域中查找,找到了 innerVarouterVar

作用域链与闭包

闭包是指函数可以访问其定义时所在的作用域,即使函数在该作用域之外执行。闭包的实现依赖于作用域链。例如:

function outer() {
    var outerValue = 10;

    function inner() {
        return outerValue;
    }

    return inner;
}

var closure = outer();
console.log(closure()); // 输出: 10

在上述代码中,outer 函数返回了 inner 函数,inner 函数形成了闭包。即使 outer 函数已经执行完毕,inner 函数仍然可以通过作用域链访问到 outerValue

执行上下文

执行上下文是 JavaScript 执行代码时的环境,它包含了变量对象、作用域链和 this 值。每次调用函数时,都会创建一个新的执行上下文。

执行上下文的类型

JavaScript 中有三种执行上下文类型:

  1. 全局执行上下文:在脚本开始执行时创建,只有一个全局执行上下文。在浏览器环境中,全局执行上下文的 this 指向 window 对象;在 Node.js 环境中,全局执行上下文的 this 指向 global 对象。
  2. 函数执行上下文:每次调用函数时创建,函数执行上下文的 this 值取决于函数的调用方式。
  3. Eval 执行上下文:在使用 eval 函数时创建,不建议使用 eval,因为它存在安全风险且不利于代码优化。

执行上下文的创建过程

执行上下文的创建分为两个阶段:

  1. 创建阶段
    • 确定 this:根据函数的调用方式确定 this 的值。例如,在全局执行上下文,this 指向全局对象;在函数执行上下文,若函数作为对象的方法调用,this 指向该对象,若以普通函数调用,this 指向全局对象(严格模式下为 undefined)。
    • 创建变量对象:在函数执行上下文,变量对象包含函数的参数、函数声明和变量声明。变量声明会被提升到作用域顶部,但赋值不会提升。在全局执行上下文,变量对象就是全局对象。
    • 构建作用域链:将变量对象添加到作用域链的前端,然后再添加上层作用域的变量对象,形成作用域链。
  2. 激活/代码执行阶段
    • 变量赋值,函数引用,执行其他代码。

例如:

function test(a, b) {
    var localVar = 'I am local';
    function innerFunction() {}

    console.log(a + b + localVar);
}

test(1, 2); 

在调用 test 函数时,创建函数执行上下文:

  1. 创建阶段
    • this:由于 test 是以普通函数调用,this 指向全局对象(非严格模式下)。
    • 变量对象:变量对象包含参数 ab,函数声明 innerFunction 和变量声明 localVarab 被初始化为传入的值 12localVar 被初始化为 undefined(因为变量声明提升但赋值未提升),innerFunction 被初始化为函数引用。
    • 作用域链:作用域链前端是 test 函数的变量对象,后面连接全局作用域的变量对象。
  2. 激活/代码执行阶段
    • localVar 被赋值为 'I am local'
    • 执行 console.log(a + b + localVar),计算并输出结果 3I am local

执行上下文栈

执行上下文栈(也称为调用栈)是 JavaScript 引擎用来管理执行上下文的一种数据结构。当 JavaScript 代码开始执行时,会先将全局执行上下文压入栈中。每当调用一个函数时,就会创建一个新的函数执行上下文并将其压入栈顶。当函数执行完毕,该函数的执行上下文会从栈顶弹出,控制权交还给之前的执行上下文。例如:

function first() {
    console.log('Inside first function');
    second();
    console.log('Again inside first function');
}

function second() {
    console.log('Inside second function');
}

first();
// 输出:
// Inside first function
// Inside second function
// Again inside first function

在上述代码中,当脚本开始执行,全局执行上下文被压入执行上下文栈。调用 first 函数时,first 函数的执行上下文被压入栈顶。在 first 函数中调用 second 函数,second 函数的执行上下文又被压入栈顶。当 second 函数执行完毕,其执行上下文从栈顶弹出,first 函数继续执行,最后 first 函数执行完毕,其执行上下文也从栈顶弹出,只剩下全局执行上下文。

作用域链与执行上下文的关系

作用域链是执行上下文的一个重要组成部分。每个执行上下文都有自己的作用域链,作用域链决定了在该执行上下文中变量和函数的查找规则。当一个函数执行上下文被创建时,其作用域链就已经确定,它包含了该函数所在作用域以及上层作用域的变量对象。例如:

var globalValue = 'global';

function outer() {
    var outerValue = 'outer';

    function inner() {
        var innerValue = 'inner';
        console.log(globalValue);
        console.log(outerValue);
        console.log(innerValue);
    }

    inner();
}

outer();
// 输出:
// global
// outer
// inner

inner 函数的执行上下文中,其作用域链包含了 inner 函数自身的变量对象(包含 innerValue)、outer 函数的变量对象(包含 outerValue)和全局变量对象(包含 globalValue)。当 inner 函数查找变量时,会沿着这个作用域链依次查找,直到找到对应的变量。

作用域链与执行上下文的实际应用

  1. 模块化开发:通过函数作用域和闭包,可以实现模块化开发。例如,使用立即执行函数表达式(IIFE)创建一个独立的模块作用域,避免全局变量污染。
// 模块模式
var myModule = (function () {
    var privateVar = 'private';

    function privateFunction() {
        console.log('This is a private function');
    }

    return {
        publicFunction: function () {
            privateFunction();
            console.log(privateVar);
        }
    };
})();

myModule.publicFunction(); 
// 输出:
// This is a private function
// private

在上述代码中,IIFE 创建了一个独立的作用域,privateVarprivateFunction 是私有的,只能通过 publicFunction 访问,实现了模块的封装。

  1. 事件处理:在事件处理函数中,作用域链和执行上下文也起着重要作用。例如:
var button = document.getElementById('myButton');
var globalMessage = 'Global message';

function clickHandler() {
    var localMessage = 'Local message';
    console.log(globalMessage);
    console.log(localMessage);
}

button.addEventListener('click', clickHandler);

当按钮被点击时,clickHandler 函数的执行上下文被创建,其作用域链包含了函数自身的变量对象(包含 localMessage)和全局变量对象(包含 globalMessage),因此可以访问到这两个变量。

  1. 错误处理与调试:理解作用域链和执行上下文有助于调试代码。例如,当变量未定义的错误发生时,可以通过分析作用域链来确定变量应该在哪个作用域中定义。
function errorFunction() {
    console.log(nonExistentVar);
}

errorFunction(); 
// 报错: nonExistentVar is not defined

在上述代码中,通过查看 errorFunction 的作用域链,可以发现 nonExistentVar 既不在函数自身作用域,也不在上层作用域中定义,从而定位错误。

总结与注意事项

  1. 作用域链的查找性能:作用域链查找变量是从当前作用域开始向上查找,查找的层级越多,性能越低。因此,尽量避免在多层嵌套的作用域中频繁查找变量。
  2. this 的绑定:正确理解 this 在不同执行上下文中的绑定规则非常重要,错误的 this 绑定可能导致难以调试的错误。例如,在回调函数中,this 的值可能与预期不同,需要使用 bindcallapply 方法来正确绑定 this
  3. 变量声明与提升var 声明的变量会被提升到作用域顶部,而 letconst 不存在变量提升。在编写代码时,要注意变量声明的位置和提升带来的影响,避免出现意外的行为。
  4. 闭包的内存管理:闭包会保持对其定义时所在作用域的引用,如果闭包使用不当,可能导致内存泄漏。例如,在闭包中引用了大量的 DOM 元素,而这些元素在闭包外部不再需要,但由于闭包的引用,它们无法被垃圾回收机制回收。

总之,深入理解 JavaScript 中的作用域链与执行上下文是编写高质量、可维护 JavaScript 代码的关键。通过合理运用它们,可以实现模块化开发、优雅的事件处理,并有效地调试代码。同时,要注意避免因作用域链和执行上下文相关问题导致的性能问题和错误。