面试官:你知道为什么var存在声明提升嘛? 纯干货-JavaScript预编译机制!!!小白必看!

241 阅读10分钟

前言

当面试官问到你:为什么var声明变量会存在声明提升?嘛我:啊?

为什么var声明对象会存在声明提升呢?今天我们来给大家解答!----预编译!!

今天我们将会带大家由浅入深去了解JavaScript中预编译的相关原理!!

正文

预编译的概念

在JavaScript中,预编译是指在代码执行前,由JavaScript引擎对代码进行一次全面检查和分析的过程。在这个过程中,JavaScript引擎会识别出代码中的变量声明、函数声明以及其他一些语法结构,并将它们按照一定的规则进行处理。处理后的代码将被放在一个对象中,以便在程序运行时能够更快地被加载和执行。

在JavaScript中,预编译(也称为预处理)是代码执行前进行的一项操作。预编译的主要目的是把变量声明和函数声明提前,并将它们按照一定的规则,放在创建的对象里面去。

JavaScript中的预编译:提高代码效率的关键

在JavaScript中,预编译是一种在代码执行前进行的处理过程。通过预编译,JavaScript引擎可以更好地解析和执行代码,从而提高代码的运行效率。

今天我们就通过分析一些代码案例来逐步学习JavaScript预编译处理的过程!!

上代码:

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

我们拿到这样一段简单代码,相信大家很容易就会知道输出结果是什么。

输出:123

我们再看这样一个案例

	foo()
    function foo(){
        console.log(a)  
        var a =123  
     }
输出:undefined

在这里,我们的输出没有报错,而是undefined,说明a这个变量已经存在,但是没有给它赋值!!这就是var声明提升带来的结果!

那么我们要如何解释它是如何提升的呢?

看案例-->

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

}
foo(3)

这里的代码输出结果会是什么呢?是 1?还是 3?还是?

我们直接看结果:

输出:2

为什么是这样的一个结果呢?今天我们就隆重介绍今天的主角之一:

Action Object 预编译发生在函数执行之前 (四部曲)

  1. 创建AO对象(Action Object)执行上下文对象
  2. 找形参和变量声明(有效标识符)将变量声明和形参作为AO的属性名,值为undefined
  3. 将实参和形参值统一
  4. 在函数体内找函数声明,将函数名作为AO对象的属性名,值赋予函数体

当预编译发生在函数执行之前,预编译的过程会遵守以上四点,我们拿出一个具体案例介绍一下,加深理解:

function fn(a)
{
    console.log(a) //function a(){}
    var a = 123 
    console.log(a)
    function a(){}//函数声明
    var b = function(){} //函数表达式
    console.log(b)
    function d(){}
    var d =a
    console.log(d)
}
fn(1)
输出:
[Function: a]
123
[Function: b]
123

从这里出发,根据我们的四部曲进行分析,首先我们编译器会在后台创建AO对象--执行上下文对象

开始对我们的代码进行编译,在全局作用域时(此处学习可以跳转到我的另一篇文章:梦回JavaScript之作用域--小白篇 - 掘金 (juejin.cn)

我们编译器不会编译函数体部分,当识别**fn(1)**编译器开始对函数体进行操作

进入到函数体当中:我们的编译器对函数体进行编译!:unicorn:

第一步:创建AO对象(Action Object)执行上下文对象

创建好AO对象之后呢,开始第二步!

第二步:找形参和变量声明(有效标识符)将变量声明和形参作为AO的属性名,值为undefined

首先,我们的编译器会找到形参a吧!于是我们编译器会把a记录在AO对象当中:a:undefined

然后又识别到var a,因为在对象当中,两个属性名相同的,后来的会把之前的的值给代替掉,但是我们的a仍然是undefined

接着,又识别到var b,于是我们的编译器把b记录到AO对象当中: b:undefined

再往下走,识别到var d ,于是编译器把d记录到AO对线当中:d:undefined

接下来,就开始我们的第三步

第三步:将实参和形参值统一

这里,我们的编译器开始将形参的值和实参的值进行统一,上述代码当中,我们形参就是a,而实参的值为1

于是我们的编译器会把AO对象当中:a:undefined-> 1

到这里,我们第三步就结束了,紧接着就开始了我们的第四步。

第四步:在函数体内找函数声明,将函数名作为AO对象的属性名,值赋予函数体

在上述代码当中,从上往下执行,我们的V8引擎(代指编译器)会识别到两条函数声明语句分别是a和b

于是呢,V8引擎就会对AO对象中的值,进行重新赋值!分别是:

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

b:undefined->function b(){}

d:undefined->function b(){}

到这里

我们的预编译就到这里结束了,于是开始执行我们的代码。我们来回顾一下:

function fn(a)
{
    console.log(a) //function a(){}
    var a = 123 
    console.log(a)
    function a(){}//函数声明
    var b = function(){} //函数表达式
    console.log(b)
    function d(){}
    var d =a
    console.log(d)
}
fn(1)
  1. 第一行开始,首先打印a,这里回到我们的AO对象当中,发现现在a的值为function a(){}于是打印了这个Function a
  2. 第二行,进行赋值。于是我们AO对象中a的值会变为:a:undefined-> 1 ->function a(){}(此处打印)->123
  3. 第三行:再次打印a,发现AO对象当中的值变为了123,于是打印123。
  4. 第四行:a函数体,执行下一行。
  5. 第五行:b函数声明,执行下一行。
  6. 第六行:打印b,回到我们AO对象当中,发现此时b的值为function b(){},这里的输出结果就是Function b
  7. 第七行:d的函数体,执行下一行
  8. 第八行:对d赋值,此时的a值为123,于是AO对象中:d:undefined->function b(){}->123
  9. 第九行:输出d,也就是输出d最后的值123

于是我们就得出了这样结果:

输出:
[Function: a]
123
[Function: b]
123

我们的AO对象为:

//fn的作用域就是fn的AO对象
 AO:{    
 	 a:undefined-> 1 ->function a(){}(此处打印)->123(此处打印)
  	 b:undefined->function b(){}(此处打印)
     d:undefined->function b(){}->123(此处打印)
 }

这是函数体执行前发生的预编译,接下来,我们学习:

Global Object 当预编译发生在全局的时候 (三部曲)

  1. 创建GO对象 (Global Object)
  2. 找变量声明(有效标识符)将变量声明作为AO的属性名,值为undefined
  3. 在全局找函数声明,将函数名作为GO对象的属性名,值赋予函数体

当预编译发生在全局的时候,我们来分析这个案例(三部曲和四部曲相结合):

global = 100
function fn(){
    console.log(global)//undefined
    global = 200
    console.log(global)//200
    var global =300
}
fn()
var global
输出:
undefined
200

我们就拿着这个案例进行分析:在全局作用域当中时预编译的执行过程如下:

第一步:创建GO对象 (Global Object)

当预编译发生在全局作用域时,我们的编译器,首先会自动创建一个GO对象,紧接着第二步。

第二步:找变量声明(有效标识符)将变量声明作为AO的属性名,值为undefined

首先,我们的编译器会找到全局作用域当中的变量声明,上述代码中有一个var global,于是编译器会在GO对象中创建一个记录

global:undefined,第二步就到这里结束了。

第三步:在全局找函数声明,将函数名作为GO对象的属性名,值赋予函数体

于是,第三步开始之后找函数声明,在我们全局作用域当中就只有fn()这一个函数名,于是会在GO对象当中创建一个

fn:function fn(){}到这里我们就开始执行全局作用域当中的代码。

回顾代码:

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

第一行开始执行:global赋值为100修改GO对象当中的global:undefined->100

第二行是函数体,往下执行直到第八行

第八行调用函数,来到函数体内部,于是我们的预编译发生在函数体执行之前,回到我们的四部曲

第一步:创建AO对象(Action Object)执行上下文对象

首先创建AO对象开始执行第二步。

第二步:找形参和变量声明(有效标识符)将变量声明和形参作为AO的属性名,值为undefined

在此处没有定义形参,只有变量声明,于是在AO对象中加入:global:undefined

第三步:将实参和形参值统一

在这里没有变化,因为没有实参和形参,于是开始第四步。

第四步:在函数体内找函数声明,将函数名作为AO对象的属性名,值赋予函数体

此处函数体内没有函数声明,开始执行函数体。

我们将函数体部分拿下来(方便观察)。

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

首先执行第一行,此处先调用的是AO对象,因为是函数体内执行,所有先调用本身作用域,再往外层作用域查找。

此时AO对象当中的global:undefined,于是此刻输出undefined

第二行,赋值,修改AO对象当中的值global:undefined(输出)->200

第三行,输出global同理,先找自身作用域输出global等于200

第四行,赋值同样是修改AO对象中的值:global:undefined(输出)->200(输出)->300

最后,我得到的GO对象和AO对象是这样的

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

输出结果也是这样

输出:
undefined
200

到这里,JavaScript预编译的四部曲和三部曲就介绍完毕了!!

最后!!我们再用图解学习一下

为什么去执行上下文查找?

因为在编译的时候会后台创建一个调用栈(存放一个又一个执行上下文),存入全局执行上下文GO,先入栈的在下面

在全局执行上下文GO中,一类是变量环境(var 声明的变量),一类是词法环境(let和const声明的对象)

在FN执行上下文 变量环境和词法环境

始终都有一个指针,调用栈的指针,时刻都在记录着当前调用栈哪些上下文正在运行,如过在当前找不到,指针会下移,这个是由调用栈决定的

如图:

调用栈.jpg

好了!!我们今天对JS预编译的学习就到这里结束啦!!还有问题的小伙伴或者想指正的小伙伴欢迎大家在评论区留言!!

玫瑰.jpeg 相信,面试官的问题:为什么var声明会存在声明提升!大家学完也能用自己的语言解释了!点赞收藏支持一下吧!!!