3

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

 3 years ago
source link: https://segmentfault.com/a/1190000040768864
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

发布于 今天 15:25

javascript代码解析过程

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

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

全局上下文

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

console.log(user)
var user = 'alice' 
var num = 10
console.log(num)

以上代码经过如下步骤才被执行

  1. javascript --> ast

    • 全局创建一个GO( GlobalObject)对象,GO中有很多内置模块,如Math、String、以及window属性,其中window的值为this,也就是自身GO对象
    • 全局定义的变量user和num会添加GO对象中,并赋值为undefined
  2. ast --> Ignition

    • V8引擎执行代码时,存在调用栈(ECStack),此时创建全局上下文栈(Global Excution Context)
    • GEC存在VO(variable Object),在全局上下文中指向GO对象
  3. 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") 
}

以上代码经过如下步骤才被执行

  1. javascript --> ast

    • 全局创建一个GO( GlobalObject)对象,GO中有很多内置模块,如Math、String、以及window属性,其中window的值为this,也就是自身GO对象
    • 全局定义的变量name会添加GO对象中,并赋值为undefined
    • 函数foo会开辟一块内存空间,比如为0x100,用来存储父级作用域(parent scope)和自身代码块,函数foo的父级作用域就是全局对象GO
    • 将foo添加到GO对象中,赋值为内存地址,如0x100
  2. ast --> Ignition

    • V8引擎执行代码时,存在调用栈(ECStack),此时创建全局上下文栈(Global Excution Context)
    • GEC存在VO(variable Object),在全局上下文中指向GO对象
  3. 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() 
} 

以上代码经过如下步骤才被执行

  1. javascript --> ast

    • 全局创建一个 GO( GlobalObject)对象,GO中有很多内置模块,如Math、String、以及window属性,其中window的值为this,也就是自身GO对象
    • 全局定义的变量user会添加GO对象中,并赋值为undefined
    • 函数foo会开辟一块内存空间,比如为0x100,用来存储父级作用域(parent scope)和自身代码块,函数foo的父级作用域就是全局对象GO
    • 将foo添加到GO对象中,赋值为内存地址,如0x100
  2. ast --> Ignition

    • V8引擎执行代码时,存在调用栈(ECStack),此时创建全局上下文栈(Global Excution Context)
    • GEC存在VO(variable Object),在全局上下文中指向GO对象
  3. 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()

以上代码经过如下步骤才被执行

  1. 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
  2. ast --> Ignition

    • V8引擎执行代码时,存在调用栈(ECStack),此时创建全局上下文栈(Global Excution Context)
    • GEC存在VO(variable Object),在全局上下文中指向GO对象
  3. 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)

执行过程如下

  1. javascript --> ast

    • GO对象中将n定义为undefined
    • 开辟foo函数的内存空间0x100,父级作用域为GO
    • 将foo添加到GO对象中,值为0x100
  2. 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()

执行顺序如下

  1. javascript --> ast

    • GO对象中添加变量n,值为undefined
    • 为函数foo开辟内存空间0x300,父级作用域为GO
    • 将foo添加到GO对象中,值为0x300
  2. 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()

执行过程如下

  1. javascript --> ast

    • GO对象中将a定义为undefined
    • 开辟foo函数的内存空间0x400,父级作用域为GO
    • 将foo添加到GO对象中,值为0x400
  2. 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) 

执行过程如下

  1. javascript --> ast

    • 创建GO对象,GO对象为空
    • 开辟foo函数的内存空间0x500,父级作用域为GO
    • 将foo添加到GO对象中,值为0x500
  2. 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高级,还有很多需要开发者掌握的地方,可以看看我写的其他博文,持续更新中~


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK