乐趣区

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

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 为空对象
  3. 赋值

    • 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
  3. 赋值

    • 将 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
  3. 赋值

    • 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
  3. 赋值

    • 执行 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 高级,还有很多须要开发者把握的中央,能够看看我写的其余博文,继续更新中~

退出移动版