一.预编译
预编译是 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();//函数提升
(二)函数中的预编译过程
每当函数被调用时,都会经历预编译:
- 创建执行上下文对象(Activation Object,AO) :在函数被调用时,JavaScript 引擎会创建一个执行上下文对象,用来存储函数执行过程中的变量、函数等信息。
- 识别变量声明:JavaScript 引擎会找出函数中的形参和变量声明,并将它们添加到执行上下文的 AO 中,初始值为
undefined
。 - 参数传递:函数被调用时,传递的实参与函数定义的形参进行对应,形成 AO 中的初始化过程。
- 函数声明处理:在函数体内部,JavaScript 引擎会找到所有的函数声明,将函数名作为 AO 的属性,并且将函数体赋值给这个属性。
(三)全局的预编译过程
在全局代码执行之前,也会进行预编译:
- 创建全局执行上下文对象(Global Object,GO) :全局代码在执行之前,JavaScript 引擎会创建一个全局的执行上下文对象。
- 识别全局变量声明:JavaScript 引擎会查找全局作用域中的所有变量声明,并将它们添加到全局执行上下文的 GO 中,初始值为
undefined
。 - 函数声明处理:同样地,JavaScript 引擎会在全局作用域中查找所有的函数声明,并将函数名作为 GO 的属性,并且将函数体赋值给这个属性。
二.调用栈(Call Stack)
调用栈是 JavaScript 引擎用来管理函数调用关系的一种数据结构。每当一个函数被调用时,都会创建一个新的执行上下文,并且推入调用栈的顶部。当函数执行完成后,对应的执行上下文会从调用栈中弹出。
(一)如何工作?
-
函数调用和执行上下文:
- 每当 JavaScript 引擎执行一个函数时,它会为该函数创建一个执行上下文(Execution Context)。
- 执行上下文包含了函数执行过程中所需的所有信息,如变量、函数参数、函数的调用位置等。
-
调用栈的操作:
- 当函数被调用时,其对应的执行上下文被推入调用栈的顶部(入栈操作)。
- 当函数执行完成或返回时,其执行上下文从调用栈中弹出(出栈操作)。
-
后进先出原则:
- 调用栈遵循后进先出(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);
的过程中,调用栈的变化如下:
printSquare(5)
被调用,它的执行上下文被推入调用栈。- 在
printSquare
函数内部调用square(5)
,square
函数的执行上下文被推入调用栈。 square
函数内部调用multiply(5, 5)
,multiply
函数的执行上下文被推入调用栈。multiply
函数执行完毕并返回结果,其执行上下文从调用栈中弹出。square
函数获取到结果并返回,其执行上下文从调用栈中弹出。printSquare
函数获取到结果并返回,其执行上下文从调用栈中弹出。
三.总结
通过预编译和调用栈,JavaScript 能够确保在代码执行时,所有变量和函数都已经被正确地识别和初始化,从而保证代码的顺利执行。而深入理解 JavaScript 的预编译和调用栈机制对于开发者来说是至关重要的,它们不仅影响代码执行的正确性和性能,还能帮助开发者更高效地进行调试和优化