JavaScipt 的执行机制
今天来学习一下 js 的执行机制。来解答下 js 中为什么会存在声明提升这一问题。
1. js 执行流程
- 读取代码
- 编译
- 执行
重要的是:代码先编译再执行
2. 执行上下文
要了解js的执行机制,首先我们要了解什么是执行上下文,我们先来看一段代码:
showName()
console.log(myname);
var myname = '小明'
function showName() {
console.log('函数showName执行了');
}
思考一下这一段代码的输出是什么,下面是答案:
你会发现 showName() 函数还未声明就能正常调用,为什么呢?在 v8 引擎眼里,代码是这样子的:
//===v8引擎===编译===
var myname //var定义的变量声明提升
function showName() { //函数声明整体提升
console.log('函数showName执行了');
}
showName()
console.log(myname);
myname = '小明'
我们把它分成两个部分:
执行上下文:当 JavaScript 代码运行时,会创建一个执行上下文来管理代码的执行。简单来说,它就像是一个盒子,里面装着代码执行时所需要的所有信息。其会被规划成两个空间,变量环境和词法环境。
变量环境放的是 var 的变量声明、函数声明,词法环境放的是 let 和 const 的变量声明, v8 引擎从中整理出可执行代码块。最后执行出结果。
执行上下文有三类:全局执行上下文、函数执行上下文、块级执行上下文
3. 调用栈
首先我们来看一段代码:
function test() {
test()
}
test()//自己调用自己,无限循环
来看看它的运行结果:超出了预期的最大栈空间,也就是爆栈。
调用栈:v8引擎用来管理函数之间的调用关系的一种结构。
让我们来理一理这个代码
var a = 2
function add() {
var b =10
return a + b
}
add() //12
来看看全局的编译
接着全局代码的执行: a 被赋值为 2 , add() 函数的调用,出现 add() 这个函数的执行上下文
由此可知:函数的编译只出现在函数被调用时
add() 开始执行,b 被赋值为 10,返回 a + b ,在 add 执行上下文找不到 a,那就会去上一个执行上下文中找,让我们看一下此时的调用栈:
4. 编译的过程
- 创建执行上下文对象
- 找形参和变量声明,将形参和声明的变量名作为key值为undifined
- 统一形参和实参的值( 全局没有该步骤 )
- 在作用域里找函数声明,将函数名作为key,值为函数体
让我们再看一个例子
var a = 2
function add(b, c) {
return b + c
}
function addAll(b, c) {
var d = 10
var result = add(b, c)
return a + result + d
}
addAll(3, 6)
来看看调用栈的变化:
js 引擎读取到代码,创建一个调用栈,全局被编译,形成一个全局执行上下文。
全局执行,a = 2,addAll 函数被调用,形成一个 addAll 执行上下文。
addAll 函数执行,d = 10 ,result 的值为 add(b,c),add 函数被调用,形成一个add 执行上下文。
add 函数执行,返回 9,add 函数执行完毕,add 执行上下文被销毁。
result = 9,addAll() 返回19,addALL 函数执行完毕,addAll 执行上下文被销毁。
全局执行完毕,全局执行上下文被销毁,调用栈恢复干净的状态。
- 编译永远发生在执行的前一刻
- 全局和函数体的编译会生成执行上下文存入调用栈
- 当一个函数执行完毕后,他的执行上下文就会被销毁(出栈)
前文中这个代码输出 a 为 undifined,我们只知道存在变量声明,现在我们就知道为什么会存在声明提升了。
console.log(a);
var a = 1;
首先,调用栈里面创建一个全局上下文,a = undefined,然后全局开始执行,执行 console.log(a),此时输出 a = undefined,然后 a 赋值为 1,执行完毕,全局上下文被销毁。
5. 练习
让我们来看一道面试题
var a = 1;
function fu(a) {
var a = 2;
function a() {}
var b = a
console.log(a);
}
fu(3);
它编译结束的调用栈:
执行之后,输出为 2