简单理解 JavaScript 的预编译和调用栈机制:为什么它们是开发者必须掌握的关键?

183 阅读5分钟

一.预编译

预编译是 JavaScript 引擎在执行代码之前的一种处理过程,它涉及变量和函数声明的提升,确保在代码执行时它们已经被识别和安排。

(一)声明提升

在 JavaScript 中,所有的变量声明(使用 var 关键字声明的变量)和函数声明会在代码执行之前被“提升”到当前作用域的顶部。这意味着在任何代码执行之前,JavaScript 引擎会将它们的声明处理如下:

  • 变量声明:变量的声明会被提升,但是初始化的赋值不会被提升。这意味着变量名会被添加到作用域的顶部,并且初始值是 undefined

     // 我们眼里的:
      var a=8;
      console.log(a);
      //v8 引擎眼中
        var a;
      console.log(a);
      a = 1;//声明提升
    
  • 函数声明:整个函数声明会被提升,包括函数名和函数体。这使得在声明函数之前调用函数是可行的。

    // 我们眼里的
    foo()
    function foo(){
    var a = 2;
    console.log(a);
    }
     //v8 引擎眼中
     function foo(){
    var a = 2;
    console.log(a);
    }
    foo();//函数提升
    
    

(二)函数中的预编译过程

每当函数被调用时,都会经历预编译:

  1. 创建执行上下文对象(Activation Object,AO) :在函数被调用时,JavaScript 引擎会创建一个执行上下文对象,用来存储函数执行过程中的变量、函数等信息。
  2. 识别变量声明:JavaScript 引擎会找出函数中的形参和变量声明,并将它们添加到执行上下文的 AO 中,初始值为 undefined
  3. 参数传递:函数被调用时,传递的实参与函数定义的形参进行对应,形成 AO 中的初始化过程。
  4. 函数声明处理:在函数体内部,JavaScript 引擎会找到所有的函数声明,将函数名作为 AO 的属性,并且将函数体赋值给这个属性。

(三)全局的预编译过程

在全局代码执行之前,也会进行预编译:

  1. 创建全局执行上下文对象(Global Object,GO) :全局代码在执行之前,JavaScript 引擎会创建一个全局的执行上下文对象。
  2. 识别全局变量声明:JavaScript 引擎会查找全局作用域中的所有变量声明,并将它们添加到全局执行上下文的 GO 中,初始值为 undefined
  3. 函数声明处理:同样地,JavaScript 引擎会在全局作用域中查找所有的函数声明,并将函数名作为 GO 的属性,并且将函数体赋值给这个属性。

二.调用栈(Call Stack)

调用栈是 JavaScript 引擎用来管理函数调用关系的一种数据结构。每当一个函数被调用时,都会创建一个新的执行上下文,并且推入调用栈的顶部。当函数执行完成后,对应的执行上下文会从调用栈中弹出。

(一)如何工作?

  1. 函数调用和执行上下文

    • 每当 JavaScript 引擎执行一个函数时,它会为该函数创建一个执行上下文(Execution Context)。
    • 执行上下文包含了函数执行过程中所需的所有信息,如变量、函数参数、函数的调用位置等。
  2. 调用栈的操作

    • 当函数被调用时,其对应的执行上下文被推入调用栈的顶部(入栈操作)。
    • 当函数执行完成或返回时,其执行上下文从调用栈中弹出(出栈操作)。
  3. 后进先出原则

    • 调用栈遵循后进先出(LIFO,Last In, First Out)的原则。也就是说,最后被推入栈的函数执行完毕后,才会处理前面的函数。

(二)为什么重要?

  • 管理函数调用:调用栈确保函数调用顺序的正确性,保证了函数按照正确的顺序执行和返回。
  • 处理递归:对于递归函数调用,调用栈允许多个相同函数的执行上下文同时存在,每一个都有自己的变量和参数,保证递归函数能够正确地执行和返回。
  • 错误追踪:当发生错误时,调用栈可以帮助开发者追踪到错误发生的具体位置和函数调用链,有助于调试和修复代码问题。

(三)简单示例


function multiply(a, b) {
  return a * b;
}

function square(n) {
  return multiply(n, n);
}

function printSquare(x) {
  var squared = square(x);
  console.log(squared);
}

printSquare(5);

在执行 printSquare(5); 的过程中,调用栈的变化如下:

  1. printSquare(5) 被调用,它的执行上下文被推入调用栈。
  2. 在 printSquare 函数内部调用 square(5)square 函数的执行上下文被推入调用栈。
  3. square 函数内部调用 multiply(5, 5)multiply 函数的执行上下文被推入调用栈。
  4. multiply 函数执行完毕并返回结果,其执行上下文从调用栈中弹出。
  5. square 函数获取到结果并返回,其执行上下文从调用栈中弹出。
  6. printSquare 函数获取到结果并返回,其执行上下文从调用栈中弹出。

三.总结

通过预编译和调用栈,JavaScript 能够确保在代码执行时,所有变量和函数都已经被正确地识别和初始化,从而保证代码的顺利执行。而深入理解 JavaScript 的预编译和调用栈机制对于开发者来说是至关重要的,它们不仅影响代码执行的正确性和性能,还能帮助开发者更高效地进行调试和优化