javascript代码解析过程
执行上下文和作用域是javascript中非常重要的部分,要弄清楚它们首先就要说到javascript的运行机制,javascript代码被解析经过了以下几个步骤
- Parser模块将javascript源码解析成抽象语法树(AST)
- Ignition模块将抽象语法树编译成字节码(byteCode),再编译成机器码
- 当函数被执行多次时,Ignition会记录优化信息,由Turbofan直接将抽象语法树编译成机器码
全局上下文
了解完以上javascript运行机制之后,我们来看看以下全局代码的执行方式
console.log(user)
var user = 'alice'
var num = 10
console.log(num)
以上代码经过如下步骤才被执行
-
javascript --> ast
- 全局创建一个GO( GlobalObject)对象,GO中有很多内置模块,如Math、String、以及window属性,其中window的值为this,也就是自身GO对象
- 全局定义的变量user和num会添加GO对象中,并赋值为undefined
-
ast --> Ignition
- V8引擎执行代码时,存在调用栈(ECStack),此时创建全局上下文栈(Global Excution Context)
- GEC存在VO(variable Object),在全局上下文中指向GO对象
-
Ignition --> 运行结果
- 通过VO找到GO
- 将user赋值为alice,将num赋值为10
图示如下
以上代码的执行的结果为
undefined
10
- parser模块将源代码编译为AST时,已经将user和num定义到VO对象中,值为undefined
- 打印user的时候,没有执行到user的赋值语句,所以user的值仍然为undefined
- 打印num的时候,已经执行了给num赋值的语句,所以num的值为10
函数上下文
定义函数的时候,执行方式和全局又有些不同
var name = 'alice'
foo(12)
function foo(num){
console.log(m)
var m = 10
var n = 20
console.log("foo")
}
以上代码经过如下步骤才被执行
-
javascript --> ast
- 全局创建一个GO( GlobalObject)对象,GO中有很多内置模块,如Math、String、以及window属性,其中window的值为this,也就是自身GO对象
- 全局定义的变量name会添加GO对象中,并赋值为undefined
- 函数foo会开辟一块内存空间,比如为0x100,用来存储父级作用域(parent scope)和自身代码块,函数foo的父级作用域就是全局对象GO
- 将foo添加到GO对象中,赋值为内存地址,如0x100
-
ast --> Ignition
- V8引擎执行代码时,存在调用栈(ECStack),此时创建全局上下文栈(Global Excution Context)
- GEC存在VO(variable Object),在全局上下文中指向GO对象
-
Ignition --> 运行结果
(1)执行全局代码- 通过VO找到GO
- 将name赋值为alice
- 执行函数foo前,创建函数执行上下文(Function Excution Context),存在VO指向AO对象
- 创建Activation Object,将num、m都定义为undefined
(2) 执行函数
- 将num赋值为12,m赋值为10,n赋值为20
- 函数foo执行完成,从调用栈(ECStack)栈顶弹出
图示如下
所以上面代码执行结果为
undefind
预编译
在Parser模块将javascript源码编译成AST时,还经过了一些细化的步骤
- Stram将源码处理为统一的编码格式
- Scanner进行词法分析,将代码转成token
- token会被转换成AST,经过preparser和parser模块
parser用来解析定义在全局的函数和变量,定义在函数中的函数只会经过预解析Preparser
函数中定义函数的执行顺序
var user = "alice"
foo(12)
function foo(num){
console.log(m)
var m = 10
function bar(){
console.log(user)
}
bar()
}
以上代码经过如下步骤才被执行
-
javascript --> ast
- 全局创建一个 GO( GlobalObject)对象,GO中有很多内置模块,如Math、String、以及window属性,其中window的值为this,也就是自身GO对象
- 全局定义的变量user会添加GO对象中,并赋值为undefined
- 函数foo会开辟一块内存空间,比如为0x100,用来存储父级作用域(parent scope)和自身代码块,函数foo的父级作用域就是全局对象GO
- 将foo添加到GO对象中,赋值为内存地址,如0x100
-
ast --> Ignition
- V8引擎执行代码时,存在调用栈(ECStack),此时创建全局上下文栈(Global Excution Context)
- GEC存在VO(variable Object),在全局上下文中指向GO对象
-
Ignition --> 运行结果
(1)执行全局代码- 通过VO找到GO
- 将user赋值为alice
- 执行函数foo前,创建foo函数的执行上下文(Function Excution Context),存在VO(variable Object)指向AO(Activation Object)对象
- 创建Activation Object,将num、m都定义为undefined
- 为函数bar开辟内存空间 0x200,用来存储父级作用域和自身代码,bar的父级作用域为函数foo的作用域AO+全局作用域GO
- 将bar添加到foo的AO对象中,赋值为内存地址,0x200
(2) 执行函数foo
- 将num赋值为12,m赋值为10
(3) 执行函数bar
- 创建bar的执行上下文,存在VO(variable Object)指向AO(Activation Object)对象
- 创建Activation Object,此时AO为空对象
- 函数bar执行完成,从调用栈(ECStack)栈顶弹出
- 函数foo也执行完成了,从调用栈(ECStack)栈顶弹出
所以上面代码执行结果为
undefined
alice
- m 在打印的时候还没有被赋值,所以为undefined
- 打印user,首先在自己作用域中查找,没有找到,往上在父级作用域foo的AO对象中查找,还没有找到,就找到了全局GO对象中
作用域
作用域是在解析成AST(抽象语法树)的时候确定的,与它在哪里被调用没有联系
var message = "Hello Global"
function foo(){
console.log(message)
}
function bar(){
var message = "Hello Bar"
foo()
}
bar()
以上代码经过如下步骤才被执行
-
javascript --> ast
- 全局创建一个 GO( GlobalObject)对象
- 全局定义的变量message会添加GO对象中,并赋值为undefined
- 函数foo开辟一块内存空间,为0x100,用来存储父级作用域(parent scope)和自身代码块,函数foo的父级作用域就是全局对象GO
- 将foo添加到GO对象中,赋值为内存地址,0x100
- 函数bar开辟一块内存空间,为0x200,用来存储父级作用域(parent scope)和自身代码块,函数foo的父级作用域就是全局对象GO
- 将bar添加到GO对象中,赋值为内存地址,0x200
-
ast --> Ignition
- V8引擎执行代码时,存在调用栈(ECStack),此时创建全局上下文栈(Global Excution Context)
- GEC存在VO(variable Object),在全局上下文中指向GO对象
-
Ignition --> 运行结果
(1)执行全局代码- 通过VO找到GO
- 将message赋值为Hello Global
- 执行函数bar前,创建bar函数的执行上下文(Function Excution Context),存在VO(variable Object)指向AO(Activation Object)对象
- 创建Activation Object,将message定义为undefined
(2) 执行函数bar
- 将message赋值为Hello Bar
(3) 执行函数foo
- 创建foo的执行上下文,存在VO(variable Object)指向AO(Activation Object)对象
- 创建Activation Object,此时AO为空对象
- *打印 message,此时自己作用域内没有message,向上查找父级作用域,foo的父级作用域为GO
- 函数foo执行完成,从调用栈(ECStack)栈顶弹出
- 函数bar也执行完成了,从调用栈(ECStack)栈顶弹出
所以最后输出的结果为
Hello Gloabl
图示如下
易混淆点
一、 没有通过var标识符声明的变量会被添加到全局
var n = 100
function foo(){
n = 200
}
foo()
console.log(n)
执行过程如下
-
javascript --> ast
- GO对象中将n定义为undefined
- 开辟foo函数的内存空间0x100,父级作用域为GO
- 将foo添加到GO对象中,值为0x100
-
ast --> Ignition
- 创建全局上下文,VO指向GO
- 执行foo函数前,创建函数上下文,VO对象指向AO对象
- 创建AO对象,AO为空对象
-
赋值
- GO中的变量n被赋值为100
- 执行foo函数中的赋值,因为没有var标识符声明,所以直接给全局GO中的n赋值200
所以此时打印结果为
200
二、函数作用域内有变量,就不会向父级作用域查找
function foo(){
console.log(n)
var n = 200
console.log(n)
}
var n = 100
foo()
执行顺序如下
-
javascript --> ast
- GO对象中添加变量n,值为undefined
- 为函数foo开辟内存空间0x300,父级作用域为GO
- 将foo添加到GO对象中,值为0x300
-
ast —> Ignition
- 创建全局上下文,VO指向GO
- 执行函数foo之前创建函数上下文,VO指向AO
- 创建AO对象,添加变量n,值为undefined
-
赋值
- 将GO中的n赋值为100
- 执行foo,打印n,此时先在自己的作用域内查找是否存在变量n,AO中存在n值为undefined,所以不会再向父级作用域中查找
- 将AO中n赋值为200
- 打印n,此时自己作用域中存在n,值为200
所以执行结果为
undefined
200
三、return语句不影响ast的生成
在代码解析阶段,是不会受return语句的影响,ast生成的过程中,只会去查找var 和 function标识符定义的内容
var a = 100
function foo(){
console.log(a)
return
var a = 100
console.log(a)
}
foo()
执行过程如下
-
javascript --> ast
- GO对象中将a定义为undefined
- 开辟foo函数的内存空间0x400,父级作用域为GO
- 将foo添加到GO对象中,值为0x400
-
ast --> Ignition
- 创建全局上下文,VO指向GO
- 执行foo函数前,创建函数上下文,VO对象指向AO对象
- 创建AO对象,将a添加到AO对象中,值为undefined
-
赋值
- GO中的变量a被赋值为100
- 执行foo函数,打印a,此时a没有被定义,所以输出undefined
- 执行return,return后面的代码不会执行
所以执行结果为
undefined
四、连等赋值
var a = b = 10,相当于var a = 10; b = 10
function foo(){
var a = b = 10
}
foo()
console.log(b)
console.log(a)
执行过程如下
-
javascript --> ast
- 创建GO对象,GO对象为空
- 开辟foo函数的内存空间0x500,父级作用域为GO
- 将foo添加到GO对象中,值为0x500
-
ast --> Ignition
- 创建全局上下文,VO指向GO
- 执行foo函数前,创建函数上下文,VO对象指向AO对象
- 创建AO对象,将a添加到AO对象中,值为undefined
-
赋值
- 执行foo函数,var a = b = 10,相当于var a = 10; b = 10,a变量有标识符,所以a被添加到AO对象中,赋值为10,b没有表示符,所以b被添加到全局对象GO,赋值为10
- 打印b,GO对象中能找到b,值为10
- 打印a,GO对象中没有a,且没有父级作用域,无法向上查找,此时报错
所以执行结果为
10
Uncaught ReferenceError: a is not defined
以上就是如何从javascript代码解析过程理解执行上下文与作用域提升的具体介绍,关于js高级,还有很多需要开发者掌握的地方,可以看看我写的其他博文,持续更新中~