前言
当我们的JS代码在执行前,V8引擎首先会创建一个调用栈然后开始进行预编译,准确来说,预编译是作为执行上下文的一部分,而预编译的过程会产生声明提升。对变量声明,是声明提升;对函数声明,是整体提升,这关乎到对作用域的理解,具体的细节可以查看我前面的文章提高你学习兴趣的JavaScript底层原理之作用域 。那么,我们这篇文章将会告诉大家为什么会产生声明提升这个行为,它的原理到底是什么。
执行上下文
函数体执行上下文
编译都是发生在代码执行的前一刻,在执行代码过程中,发现有函数被调用时则会进行函数体中的预编译,也就是函数执行上下文。函数执行上下文可以分成四个步骤,这里我们先列出来然后用例子进行讲解:
- 创建函数的执行上下文对象 AO(activation object)
- 找到形参和变量声明,将形参和变量名作为AO的属性,值为undefined
- 将实参与形参统一
- 在函数体内找函数声明,将函数名作为AO的属性名,值为函数体本身(如果属性名已经存在,则覆盖掉其本身的值)
function fn(a){
console.log(a); //funcation a(){},
var a = 123;
console.log(a); //123
function a(){}
console.log(a); //123
var b = function (){};
console.log(b); //funcation b(){}
function d(){}
var d = a;
console.log(d); //123
}
fn(1);
首先看到这个例子的第一眼是不是感觉眼花缭乱cpu直接冒烟了,不用着急,我们通过这个例子的输出来一步一步理清楚上面的四个步骤
首先,我们的代码在执行到fn(1)
调用函数的时候,会开始函数执行上下文,它会给我们创建一个AO:{}
对象,然后开始进行形参和变量声明的查找,将形参a和b,d的变量声明加入AO中,并将值赋为Undefined,然后进行第三步,将1的值赋给形参a,然后进行最后一步,因为a属性名已经存在了,所以在找函数声明时,会将a的值由一变更为funcation a(){}
,b的值也会变为funcation b(){}
,d的值也从Undefined变为funcation d(){}
,然后执行上下文到这为止就已经结束了,开始执行函数,将会输出
AO属性值的具体变化:
AO:{
a:undefined,-> 1,-> funcation a(){},-> 123
b:undefined,-> funcation b(){},
d:undefined,-> funcation d(){},-> 123
}
全局执行上下文
全局执行上下文是V8引擎拿到一个代码创建完调用栈后第一个进行的步骤,全局执行上下文总共分为三步:
- 创建一个全局执行上下文对象 GO(global object)
- 在全局寻找变量声明,变量名作为GO的属性名,值为undefined
- 在全局寻找函数声明,函数名作为GO的属性名,值为函数体本身
同样我们通过一个例子来理解何为全局执行上下文
var global = 100;
function fn (){
console.log(global); //100
}
首先V8引擎会创建一个GO对象用来执行全局上下文,然后开始寻找变量声明,于是向GO中加入了一个global属性,值为undefineed,然后寻找函数声明,加入了一个fn属性,值为fn(){},然后就开始执行代码了。将global赋值为100后,调用fn()函数,此时会创建一个函数执行上下文,但是里面没有任何属性,就会输出100,结束执行。
GO:{
global:undefined,-> 100
fn:fn(){},
}
这个代码非常简单,就算是前端小白看了也能一眼看出来输出是100,可是到底为什么是100呢,为什么它会先在函数体内部寻找global的值,没有找到才会去全局寻找呢,这时候我们就要引入一个非常重要的概念了————调用栈。
首先和为栈?
栈就是一种线性的数据结构,在JS中,你可以把他想象成一个只具有pop()和push()或者shift()和unshift()方法的数组,它遵循先进后出,后进先出的原则,这意味着最后添加到栈的元素将会是最先被移除的那一个。
调用栈
而我们创建的执行上下文对象则会被存储在栈中。每一个执行对象文中分别有两个空间,一个叫变量环境,里面用来存放var属性的变量,还有一个词法环境,用来存放let和const环境的变量。因为let和const是es6新增的关键字,三者具体的区别可以翻看我之前的文章一篇文章带你彻底弄懂 var let const 的区别var和let的区别里面具体讲解了三者的不同。
在函数被执行完成时,执行上下文将会销毁出栈,在全部代码执行完成时,全局执行上下文也会销毁出栈。
所以这是V8引擎专门用来追踪函数执行的一个机制,因为栈只能从上向下进行访问,所以在函数执行需要一个变量时,会先从函数执行上下文开始寻找,并且遵循先找词法环境,再找变量环境的规则,在函数执行上下文中没有找到,才会进入下一层全局执行上下种开始查找。
小结
了解JS的底层机制,不仅可以让这门语言的学习变得更为轻松,更加了解JS的执行机制,并且在大厂面试时也有机会狠狠反向拷打面试官,所以深挖JS的底层运行原理对我们来说是极为重要的,希望能对你有帮助。