面试官灵魂拷问——js预编译你了解吗

1,269 阅读8分钟

前言

在许多大厂的面试中,js的预编译经常会被面试官提及,今天,小编就带大家来深入理解一下js预编译的详细过程。

首先咱们来看一个案例:

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

在面试时,面试官问你这段代码的执行结果时,你可能会脱口而出“100”,此时,面试官来接着问你原因,问你为什么?你也可能马上答出说在函数内部作用域里找不到global这个变量的值于是去全局作用域里找到一个变量global=100,所以打印输出的值为100.答案当然是对的,但是在面试官眼里可能不太满意,如果这题10分,在面试官眼里可能只会给你六七分。因为你回答的不够深入,没有说到面试官想要的答案。那么,面试官想要的答案是什么呢?今天小编就带大家来深入理解一下js的预编译过程,看完这个,相信你就知道答案了。

一、什么是预编译?

在JavaScript中,预编译是代码执行前进行的一项操作。具体来说,预编译会把变量声明和函数声明提前,并按照一定的规则将这些声明放在创建的对象中。这个过程主要分为全局预编译和局部预编译,全局预编译发生在页面加载完成时,而局部预编译则发生在函数执行的前一刻。 简单来说,就是代码在执行前需要进行编译操作,用于确定代码之间的各种关联。

二、预编译的作用是什么?

预编译阶段主要是完成函数声明和变量声明的提升,但没有初始化行为(即赋值)。需要注意的是,匿名函数不参与预编译。在预编译过程中,JavaScript会在内存中开辟一块空间,用来存放这些变量和函数。预编译的存在有助于JavaScript引擎更有效地执行代码,因为它允许引擎在代码实际执行之前进行一些优化和准备工作。

首先给出我压箱底的预编译过程,句句重点!(敲黑板!)

发生在全局的预编译:

1.创建GO(Global Object)对象
2.找变量声明,将变量名作为GO的属性名,值为undefined
3.在全局找函数声明,将函数名作为go的属性名,值为该函数体

发生在函数体内的预编译:

1.创建一个AO(Action 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 c = a
    console.log(c)
}
fn(1)

输出结果:

Snipaste_2024-04-27_23-15-07.png 这题只有一个函数体,所以我们主要是对函数体的预编译:

步骤一:创建AO对象

AO{

}

步骤二:找到形参和变量声明后将其作为AO的属性名并赋值为undefined

AO{
 a:undefined ->undefined
 b:undefined
 c:undefined
}

这里给大家解释一下a这个属性,代码从上到下,先是因为fn(a)形参的原因赋值为undefined,后是因为var a声明的原因再次赋值为undefined,两次重复声明,取最新的,最后还是undefined

步骤三:形参与实参值统一

AO{
 a:undefined->1
 b:undefined
 c:undefined

步骤四:在函数体内找函数声明,将函数名作为AO的属性名,值为该函数体

AO{
	a: 1 -> function: a
	b: undefined
	d: undefined ->function: c
}

这里咱们需要注意,像function a(){}这样的才是函数声明,如果有=叫做函数表达式,比如var a = function(){}
好,这里我们的预编译工作已经完成

现在开始执行

AO{
	a: function: a 输出 -> 123 输出 输出
	b: undefined -> function: b 输出
	c: function: c -> 123 输出
}

执行的时候是执行带有=的赋值语句和console.log等执行语句.

案例二:

function test(a, b) {
    console.log(a);
    c = 0
    var c;
    a = 3
    b = 2
    console.log(b);
    function b() {}
    console.log(b);
  }
  test(1);

输出结果:

Snipaste_2024-04-28_08-24-05.png 此题相比第一题多了直接赋值语句,像c=0这种。该题还是主要发生在函数体内
步骤一:创建AO对象

AO{

}

步骤二:找形参和变量声明后将其作为属性名并赋值为undefined

AO{
a:undefined
b:undefined
c:undefined
}

步骤三:形参和实参相统一

AO{
a:undefined->1
b:undefined
c:undefined
}

步骤四:找函数声明将函数名作为属性名,值为该函数体

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

现在开始执行:

AO{
a:1 输出 ->3
b:function b(){} ->2 输出 输出
c:undefined ->0
}

案例三:

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

该题既有全局又有函数体,所以咱们先对全局进行分析: 步骤一:创建GO对象

GO{

}

步骤二:找表量声明并赋值undefined

GO{
global:undefined
}

步骤三:在全局找函数声明作为GO的属性名,值为该函数体

GO{
global:undefined
fn: function fn(){}
}

现在开始执行:

GO{
	global: undefined -> 100
	fn: function: fn
}

执行函数前也需要编译,所以现在咱们对函数体进行预编译 步骤一:创建一个AO对象

AO{

}

步骤二:找形参和变量声明将其作为属性名并赋值为undefined

AO{
global:undefined
}

步骤三:形参和实参相统一

AO{
global:undefined
}

步骤四:在函数体内找函数声明,并赋值为该函数体

AO{
global:undefined
}

这里的步骤三、四都没有变化,因为函数体中没有形参和函数声明,现在开始直接执行函数

GO{
global:undefined-> 100
fn:function fn(){}
}
AO{
global:undefined 输出  ->200 输出 ->300
}

所以这里的输出结果为undefined和200.相信说到这里大家应该会做这类题目啦。 那咱们在回头来看文章开头提到的那道题目,会不会有更深的理解

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

首先V8引擎会先创建一个GO对象,然后再全局作用域里找变量声明global,并作为GO的属性名,赋值为undefined,然后在全局作用域里找函数声明fn();将其作为GO的属性名并赋值为该函数体存入GO中,这样GO对象中就有属性global和fn(),接下来在全局执行,将global的值赋予100,接下来进行函数体fn的编译,先创建一个AO对象,然后再函数作用域下找形参和变量声明将形参和变量名作为AO的属性名,值为undefined,接下来形参和实参统一,最后再函数体内找函数声明,将函数名作为AO的属性名,值为该函数体存入AO中,且该AO对象指向刚刚的GO对象,可惜这题里面都没有后面几个步骤,编译完成后就进行执行,输出global的值,会先再当前自己的函数作用域里查找,找不到的话就会往全局作用域里找,如果全局作用域里也没有,则该程序就会报错,最后再全局作用域里找到一个global值为100,于是便打印输出100.如果你是这样回答面试官的,那么恭喜你,这道题面试官就会觉得你彻底理解了。

这里还要给大家介绍一个新的名词——调用栈
JavaScript的调用栈是一种栈结构,用于存储计算机程序执行时其活跃子程序的信息。它遵循后进先出的原则。在JavaScript中,每当一个函数被调用时,引擎会生成一个栈帧,该栈帧保存了函数的执行上下文(包括函数的参数、局部变量以及函数的返回地址等信息),然后将这个栈帧压入调用栈。当函数执行完成后,相应的栈帧会从栈顶弹出,控制权交回给上一个调用的函数。

Snipaste_2024-04-28_09-37-58.png

其实可以理解为被阉割后的数组,它只能先进后出或者后进先出。当一个js文件中有既有全局作用域又有函数作用域的时候V8引擎会先将全局作用域放入栈底,这里我们管它叫全局执行上下文,函数作用域这里叫函数执行上下文

全局执行上下文有两类,一类是变量环境,专门用于存放像var变量的声明,另一类是词法环境,专门用来存放let和const变量,所以这里就是let和const不会声明提升的底层原因。
全局执行上下文放入栈底后开始执行操作,global = 100fn的调用。之后函数执行上下文入栈函数执行上下文也有自己的变量环境,词法环境,存放内容与全局执行上下文一致,调用栈有个指针,指向当前在哪个上下文中执行,它会先在函数执行上下文中找词法环境,然后去到变量环境,找不到的话指针就下移,到全局执行上下文,先找词法环境,后找变量环境。

说到这里相信小伙伴们对js的预编译都有了一个更深入的理解吧。如果觉得我有理解不对的地方欢迎大家批评指正,如果觉得本文对你有帮助的话欢迎大家一键三连!感谢感谢!