前言
当面试官问到你:为什么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 预编译发生在函数执行之前 (四部曲)
- 创建AO对象(Action Object)执行上下文对象
- 找形参和变量声明(有效标识符)将变量声明和形参作为AO的属性名,值为undefined
- 将实参和形参值统一
- 在函数体内找函数声明,将函数名作为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)
- 第一行开始,首先打印a,这里回到我们的AO对象当中,发现现在a的值为function a(){}于是打印了这个Function a
- 第二行,进行赋值。于是我们AO对象中a的值会变为:a:undefined-> 1 ->function a(){}(此处打印)->123
- 第三行:再次打印a,发现AO对象当中的值变为了123,于是打印123。
- 第四行:a函数体,执行下一行。
- 第五行:b函数声明,执行下一行。
- 第六行:打印b,回到我们AO对象当中,发现此时b的值为function b(){},这里的输出结果就是Function b
- 第七行:d的函数体,执行下一行
- 第八行:对d赋值,此时的a值为123,于是AO对象中:d:undefined->function b(){}->123
- 第九行:输出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 当预编译发生在全局的时候 (三部曲)
- 创建GO对象 (Global Object)
- 找变量声明(有效标识符)将变量声明作为AO的属性名,值为undefined
- 在全局找函数声明,将函数名作为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执行上下文 变量环境和词法环境
始终都有一个指针,调用栈的指针,时刻都在记录着当前调用栈哪些上下文正在运行,如过在当前找不到,指针会下移,这个是由调用栈决定的
如图:
好了!!我们今天对JS预编译的学习就到这里结束啦!!还有问题的小伙伴或者想指正的小伙伴欢迎大家在评论区留言!!
相信,面试官的问题:为什么var声明会存在声明提升!大家学完也能用自己的语言解释了!点赞收藏支持一下吧!!!