介绍
本文是 JavaScript 高级深入浅出系列的第二篇,本文将深入剖析 JS 引擎对于变量与函数的解析与执行。
正文
1. 对于变量
第一篇中我们已经了解到,在解析 JS 代码时,内部的执行上下文栈 ECS,为了执行全局的代码块,会在内部创建一个全局执行栈 GEC,GEC 在解析阶段会创建一个全局对象:GO,并注入一些全局对象和变量 window,指向 GO 自己。
在 JS 引擎解析 JS 代码时(还未执行代码前),会将全局的所有变量注入到 GO 中,但不会进行赋值,所以值是 undefined
开始执行代码阶段,从上到下依次执行,将bar这个字符串最终赋值给 GO 中的foo,所以:
console.log(foo)
var foo = 'bar'
// result: undefined
2. 对于函数
var name = 'alex'
foo()
function foo (num) {
var n1 = 10
var n2 = 20
console.log("bar")
}
2.1 编译阶段
此时,GO 在编译阶段中应该长这样:
var GlobalObject = {
Number: 'xxx',
window: GlobalObject,
name: undefined,
foo: 0x11 // 函数体的内存地址
}
在编译阶段遇到函数,会开辟一块储存该函数信息的内存空间,其中包含以下信息:
- 父级作用域 parent scope, foo 函数的父级作用域其实就是全局作用域,也就是 VO 指向 GO
- 函数执行体
2.2 执行阶段
编译结束后,将开始从上到下,从左到右依次执行。
第一行:
var name = 'alex',将alex的值放到vo也就是go的name变量中
第二行:
foo(),遇到 () 表示为函数的调用,那么就会在全局对象 GO 中找到foo,进而找到存储函数的内存地址。
- 开始解析函数,会在 ECStack 中创建一块函数执行上下文(Funtional Execution Context, FEC)。
- 在 FEC 中也有 VO,和 GEC 的 VO 不同的是,这里的 VO 指向的是 AO(Activation Object)。
- AO 中,储存的就是函数执行体的数据,在解析阶段,会把所有的变量都放在 AO 中,但是并不赋值
foo(0)
function foo (num) {
var n1 = 10
var n2 = 20
console.log("bar")
}
// 解析阶段 ↓ 形参和声明的变量放入 AO 中,单并不赋值
var AO = {
num: undefined,
n1: undefined,
n2: undefined
}
-
在执行函数内代码阶段,传入的参数将修改 AO 中 num 的值
-
n1,n2 将被赋初值 10 和 20
-
console.log,先在本作用域(也就是函数内)找是否有console,没有就去父级作用域(这里是 GO),找到了 console,调用 log 方法,最终打印出foo
函数体内的所有代码执行完毕后,当前函数所属的 FEC 将弹出 ECStack,如果 AO 没有被调用,那么这个 AO 也会被销毁掉。如果后续再次调用foo函数,那么就会再次重复此流程,创建 FEC -> 创建 AO -> 执行 -> 销毁
总结
本文中,你学习到两个知识点:
1. 巩固解析与执行变量的过程
再次巩固第一篇中解析变量的过程,JS 源码 -> 解析阶段 -> JS 引擎内部 ECStack 创建全局执行上下文 GEC -> GEC 创建全局变量 GO ,内部的 VO 其实指向的就是 GO -> 执行代码 赋初值
2. 深入了解析与执行函数的过程
在执行全局代码时遇到了(),即代表调用函数,开始全量解析函数。
解析函数阶段:GEC 创建函数执行栈 FEC -> FEC 创建 AO ,内部的 VO 指向 AO -> AO 中初始化函数体内的变量 -> 执行代码 -> FEC 弹出 GEC , AO 销毁