JS中的预编译到底发生了什么

246 阅读4分钟

JavaScript在代码执行前,会对代码进行一系列的预编译操作,其中包括声明提升和创建执行上下文。这些机制在编写和调试代码时非常重要。本文将深入探讨声明提升、函数和全局的预编译过程,以及调用栈的作用。

预编译

声明提升

我们先来看一段代码

 console.log(a); 
 var a = 1;      

很多人应该会认为编译器会报错吧,但实际情况是不仅没有报错,还输出了值——undefined

image.png

这是因为编译器在执行代码前,会先进行预编译,产生声明提升,所以代码在编译器眼里其实是下面这样的

var a; //变量声明提升
console.log(a);
a=1;

函数同理

foo()               //function foo(){
function foo(){     //  var a = 1
    var a = 1       //  console.log(a);
    console.log(a); //}
}                   //foo() 函数声明,整体提升
  • 声明提升
  1. 变量声明,声明提升
  2. 函数声明,整体提升

函数中预编译的过程

理解了声明提升后,我们再来思考一段代码

var a = 1;
function foo(a){
    var a = 1
    function a(){}
    console.log(a);
}
foo(3)

要分析上面的代码,只知道声明提升是不够的,因为声明提升只是预编译产生的一种现象,所以你还需要知道预编译真正的规则,接下来我们就来讲一讲函数中预编译到底发生了什么。

  • 函数中的预编译
  1. 创建函数的执行上下文对象 AO(Activation Object)
  2. 找形参和变量声明,将形参和变量名作为AO的属性,值为undefined
  3. 将实参和形参统一
  4. 在函数体内找函数声明,将函数名作为AO属性名,值赋予函数体

我们按上面的步骤逐一分析刚刚的代码:

  1. 首先创建函数的执行上下文对象AO,然后找到形参a和变量声明var a,值为undefined
  2. 将实参和形参统一,即a = 3
  3. 最后找函数声明,a = function a(){},到这里预编译就完成了。
  4. 接下来就是执行了,要执行的只有var a = 1,所以现在a的值就为1,最后执行输出a的值,也就是输出1了

image.png

注意这里的var a = 1说的是函数体内的,全局的var a = 1已经在函数的预编译前处理完了,具体可以看下面全局预编译的过程。

总体就是下面这样

AO:{
    a:undefined -> 3 -> function a(){} -> 1
}

如果将输出语句放到函数体的最前面,最终结果是输出[Function: a]

var a = 1;
function foo(a){
    console.log(a);
    var a = 1
    function a(){}
}
foo(3)

image.png

因为输出的在执行var a = 1前,而此时经过预编译a的值为function a(){}

全局中预编译的过程

理解了函数的预编译,其实全局的预编译也就没问题了,全局预编译只是比函数少了个将实参和形参统一。

全局的预编译

  1. 创建全局执行上下文对象 GO(Global Object)
  2. 找变量声明,变量名作为GO的属性名,值为undefined
  3. 在全局找函数声明,函数名作为GO的属性名,值为函数体
  • 要特别注意的是,预编译是发生在执行前一刻的,所以函数声明时并不会进行预编译。

现在让我们来练习一道题

global = 100
function fn(){
    console.log(global);
    global = 200
    console.log(global);
    var global = 300
}
fn()

最后输出的值是什么呢?

解毒:

  1. 首先全局进行预编译,没有任何变量声明,继续找函数声明,找到fn,值为functio(){}
  2. 预编译完成后,执行函数fn()
  3. 函数执行前先要进行预编译,函数的预编译中找到global,值为undefined,预编译结束。
  4. 执行函数,此时global值为undefined,所以输出undefined,然后执行global=200,再输出200,最后执行global=300.

所以输出的值为200,而global最后的值为300.

调用栈

最后我们还需要知道的是,全局和函数创建的执行上下文对象是存放在一个调用栈中的,可以简单理解为是用来管理函数调用关系的一种数据结构。而浏览器的调用栈往往是非常小(为了浏览器的运行速度),那如果经常调用函数的话不是会很容易爆栈吗,所以当一个函数执行完后,他的执行上下文是会被销毁的,全局也一样。

总结

理解预编译过程对于写出健壮的JavaScript代码至关重要。这些概念解释了为何某些变量和函数在未明确声明之前就可以使用。此外,调用栈的概念有助于理解函数调用的顺序和如何调试代码。通过深入理解这些机制,开发者可以更好地掌握JavaScript的工作原理,避免常见的错误和误区。