从javascript代码解析过程理解执行上下文与作用域提升

2023-08-07

javascript代码解析过程

执行上下文和作用域是javascript中非常重要的部分,要弄清楚它们首先就要说到javascript的运行机制,javascript代码被解析经过了以下几个步骤

Parser模块将javascript源码解析成抽象语法树(AST)
Ignition模块将抽象语法树编译成字节码(byteCode),再编译成机器码
当函数被执行多次时,Ignition会记录优化信息,由Turbofan直接将抽象语法树编译成机器码

全局上下文

了解完以上javascript运行机制之后,我们来看看以下全局代码的执行方式

console.log(user)
var user = 'alice'
var num = 16
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赋值为16

图示如下

以上代码的执行的结果为

undefined
16

parser模块将源代码编译为AST时,已经将user和num定义到VO对象中,值为undefined
打印user的时候,没有执行到user的赋值语句,所以user的值仍然为undefined
打印num的时候,已经执行了给num赋值的语句,所以num的值为16

函数上下文

定义函数的时候,执行方式和全局又有些不同

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)栈顶弹出

图示如下

所以上面代码执行结果为

undefined

预编译

在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高级,还有很多需要开发者掌握的地方,可以看看我写的其他博文,持续更新中~

从javascript代码解析过程理解执行上下文与作用域提升的相关教程结束。