深入了解JS作用域链,浏览器中JS的执行机制


theme: juejin

前言

在之前的文章详细介绍了作用域和预编译,作用域链和这两者都有着紧密的联系。JavaScript 的作用域链(Scope Chain)是一个重要的概念,它决定了在代码执行过程中如何查找变量和函数。理解作用域链需要了解 JavaScript 中的词法作用域、执行上下文和作用域链的关系。

执行上下文

  • 在 JavaScript 中,执行上下文(Execution Context)是代码执行时的环境。每当 JavaScript 代码运行时,都会创建一个执行上下文。
  • 每个执行上下文都有自己的变量对象(Variable Object),用于存储变量、函数和参数。
  • 在 JavaScript 中,执行上下文对象在以下情况下会被创建:
  1. 全局执行上下文:当 JavaScript 程序开始执行时,会创建一个全局执行上下文,代表整个 JavaScript 文件的运行环境。这是代码中的最顶层执行上下文。

  2. 函数调用:每次调用函数时,都会创建一个新的执行上下文对象。这意味着每个函数调用都有自己的执行上下文,其中包含了函数的局部变量、参数以及在函数内部声明的其他内容。

  3. Eval 函数:当使用 eval() 函数执行动态代码时,也会创建一个执行上下文对象。

  • 在预编译的时候会创建执行上下文对象和可执行的代码两部分,其中执行上下文里包含变量环境和词法环境,如下图所示:
var a = 2;
function add(){
    var b = 10;
    return a + b;
}
add();

上面一段代码在预编译时先创建一个全局执行上下文,如下图所示,a的值在预编译时被赋值为undefined,在执行代码时被赋值为2

当执行到函数add的调用时,会创建一个add执行上下文,如下图所示:

而执行上下文是通过什么管理的呢,这就不得不提到栈结构了,JS引擎就是通过栈结构来管理执行上下文的。

调用栈

  • 栈结构:特殊的数组,后进先出

下面的一段代码你知道会打印出来的结果是什么吗?

function foo(){
    var a = 1;
    let b = 2;
    {
        let b = 3;
        var c = 4;
        let d = 5;
        console.log(a);//1
        console.log(b);//3
    }
    console.log(b);//2
    console.log(c);//4
    console.log(d);//error
}
foo();

在执行上下文中查找值是先在词法环境中找,在词法环境中不可以找到值再去变量环境中找值,这也是为什么第9行代码中打印出来的结果是3,因为词法环境里面是一个栈结构,在foo函数中先声明let b = 2,先将这个b压入栈底,后面声明let b = 3,再将其压入栈中,查找时是从上往下,所以查找出来b的值为3。

  • 调用栈: JavaScript 中的调用栈是一个用于管理函数调用的数据结构,它遵循后进先出(LIFO)的原则。每当执行一个函数时,其对应的执行上下文会被推入调用栈,当函数执行完成后,执行上下文会被弹出。这个过程反复进行,直到调用栈为空。

举个例子:

function firstFunction() {
    console.log("Inside firstFunction");
    secondFunction();
}

function secondFunction() {
console.log(“Inside secondFunction”);
thirdFunction();
}

function thirdFunction() {
console.log(“Inside thirdFunction”);
}

firstFunction();

在执行这段代码时,调用栈的变化如下:

  1. 程序开始执行时,创建全局执行上下文,并将其推入调用栈中。
  2. firstFunction 被调用,创建 firstFunction 的执行上下文,并将其推入调用栈中。
  3. firstFunction 中调用 secondFunction,创建 secondFunction 的执行上下文,并将其推入调用栈中。
  4. secondFunction 中调用 thirdFunction,创建 thirdFunction 的执行上下文,并将其推入调用栈中。
  5. thirdFunction 执行完毕,其执行上下文从调用栈中弹出。
  6. secondFunction 执行完毕,其执行上下文从调用栈中弹出。
  7. firstFunction 执行完毕,其执行上下文从调用栈中弹出。
  8. 程序执行完毕,全局执行上下文从调用栈中弹出。

在这个过程中,调用栈记录了函数调用的顺序和当前的执行位置。如果函数调用嵌套太深,或者出现了递归调用没有终止条件,就可能导致调用栈溢出错误(stack overflow)。

  • 栈溢出: 调用栈的内存超出限制

作用域链

在 JavaScript 中,每个函数都会创建一个新的执行上下文,每个执行上下文都有自己的变量对象。当函数被调用时,会创建一个新的执行上下文,并将其推入执行上下文栈(Execution Context Stack)中,形成一个执行上下文的链式结构,即作用域链。

function bar(){
    console.log(myName);// 结果为Jerry
}

function foo(){
var myName = ‘Tom’;
bar();
}

var myName = ‘Jerry’;

foo();

在作用域链中,outer 指向的是外部(上一级)执行上下文的作用域。这与词法作用域有关,bar函数声明在了全局作用域里,所以bar的outer指向全局执行上下文,同理,foo函数的声明也在全局作用域里,所以foo的outer也指向全局执行上下文。所以最终的打印结果是Jerry。

  • 作用域链并不是在调用栈中从上到下查找,而是看当前执行上下文变量环境中的outer指向来定,而outer指向的规则是,我的词法作用域在哪里,outer就指向哪里。
  • 词法作用域: 函数定义是所在时所在的作用域

结语

理解作用域链对于理解 JavaScript 中的变量作用域和作用域嵌套非常重要,这对于编写可维护、可扩展的代码至关重要。希望这篇文章可以给你带来帮助。


这是一个从 https://juejin.cn/post/7368401073089298486 下的原始话题分离的讨论话题