深入探讨老板的女秘书:js中的预编译?

230 阅读5分钟

javaScript是一门非常强大的语言,它既能做前端也能做后端;在JavaScript中,代码在执行之前有一个非常重要的步骤,那就是预编译,它会帮助确定变量和函数的作用域,并为它们分配内存空间。预编译发生在代码执行前,这种工作方式就犹如女秘书一般,在老板工作之前,把任务一一安排好;方便后续工作有条不紊的进行。今天就让我们一起撕破“女秘书”神秘的面纱

image.png

预编译

声明提升

编译就是整理代码中的规则,让 v8(javaScript的执行引擎) 能够读懂我们写的代码;首先来看下面一段代码:

console.log(a);
var a = 123

正常来说,你看到代码的第一眼是会觉得它是会报错的;因为变量的声明在执行操作的下面,所以它读取不到a的值;但是这段代码在v8执行之后会输出 undefined ,v8之所以能够输出这个值,就是取决于js中的预编译

预编译主要会干以下三个步骤:

  1. 词法分析
  2. 解析
  3. 生成代码

所以在v8眼里,执行完预编译之后代码应该是这样的;

var a;    // 声明一个变量a,不赋值,所以为undefined
console.log(a);
a = 123;  // 进行赋值操作

从上面可以看到,预编译会把var声明的变量给提升到前面;对于变量,只有使用var声明的变量会被提升,而letconst声明的变量不会被提升,它们具有块级作用域。所以要是换成let声明,以上代码就会报错。

同样的,函数声明(function declaration)也会被提升

foo()

// 函数声明提升

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

image.png

需要注意的是,在声明提升当中;变量声明时,只有它的声明会被提升前面,对这个变量的赋值操作并不会一起提升;而在函数声明中,整个函数体都会被提升到前面

函数中的预编译

函数中的预编译主要会执行以下的一些步骤:

  1. 创建函数的执行上下文对象 AO {Activation Object}
  2. 找形参和变量声明,将形参和变量名作为AO的属性,值为undefined
  3. 将实参和形参统一
  4. 在函数体内找函数声明,将函数名作为AO的属性名,值赋予函数体
function fn(a) {
    console.log(a);
    var a = 123
    console.log(a);
    function a() {}
    console.log(a);
    var b = function () {}
    console.log(b);
    function d() {}
    var d = a
    console.log(d);
  }
  fn(1);

以上面的代码为例,我们可以模拟一下函数中预编译的过程:

// 创建函数的执行上下文对象,AO
AO:{
     a: undefined -> 1 -> function () {} -> 123,
     b: undefined -> function () {},
     d: undefined -> function () {},
    }

在上面的预编译过程中,首先“女秘书”会先创建函数的执行上下文对象,AO;然后,开始找到函数中的形参和变量声明,找到三个变量声明a、b、d,并把它们的值都赋为,undefined,再添加到AO对象中;接着,把函数的形参和实参统一,即由fn(1),可以把形参a变为1;最后,再找到函数中的函数声明,function a(),b(),d();由于在AO对象中不能出现两个相同的key,所以“女秘书”直接会把a,b,d的值修改为函数体function () {};这样预编译的工作就执行完了。

image.png

全局的预编译

“女秘书”在完成函数的预编译后,同样也要完成全局中的预编译过程;主要步骤如下:

  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()
var global;

我们再来模拟一下全局的预编译过程:

GO:{
    global: undefined -> 100,
    fn: function(){},
}

AO:{
    global: undefined
}

首先创建全局的执行上下文对象,GO;再找变量声明,找到变量global,变量名作为GO的属性名,值赋为undefined;最后在全局找函数声明,找到函数fn,函数名fn作为GO的属性名,值赋为函数体function(){};这样就完成了全局的一个预编译过程。

image.png

调用栈

何为调用栈?调用栈(Call Stack)是编程中的一个重要概念,特别是在涉及函数调用和执行流的跟踪时。它是一个数据结构,通常以栈(Last In, First Out, LIFO)的形式存储函数调用的信息。每当一个函数被调用时,相关信息(如函数返回地址、局部变量、参数等)会被压入(push)调用栈的顶部;当函数执行结束并返回时,相关信息会被弹出(pop)栈,控制权返回到调用者。

var a = 2
function add() {
var b = 10
return a + b
}
add()

其实在预编译完成后,代码执行时,就会把预编译过程中创建的GO和AO对象给逐一压入调用栈;然后v8就会顺着调用栈的顺序从上往下查找需要的变量。

lQLPJx9jO7_SEkHNBDjNB4CwZ9nRjSN5C6UGPE8-mPPCAA_1920_1080(1).png

调用栈是用来管理函数调用关系的一种数据结构,每当执行完一个函数的调用,这个函数的执行上下文对象就会出栈然后被销毁。

49547B4D-3835-4FD7-9444-0A4B9EC00BF3(1).png

小结

在js中,预编译和调用栈是理解JavaScript执行流程中的两个重要概念;预编译发生在函数即将执行前,是JavaScript引擎为函数执行做准备的一个阶段;预编译阶段主要处理函数内部的环境和变量声明,确保函数准备好执行;调用栈负责管理函数调用的执行顺序,保证程序逻辑的有序执行和正确的控制流。

你明白了吗?

image.png