javascript代码解析过程

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

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

全局上下文

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

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

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

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

从javascript代码解析过程理解执行上下文与作用域提升的更多相关文章

  1. 《浏览器工作原理与实践》<07>变量提升:JavaScript代码是按顺序执行的吗?

    讲解完宏观视角下的浏览器后,从这篇文章开始,我们就进入下一个新的模块了,这里我会对 JavaScript 执行原理做深入介绍. 今天在该模块的第一篇文章,我们主要讲解执行上下文相关的内容.那为什么先讲 ...

  2. javascript的解析过程

    引言: javascript是一种解释型的脚本语言,它不同于java或者c#这种编译语言,不需要编译成游览器可识别的语言,而是由游览器动态解析和执行的.(本身就是游览器可以直接识别,javascrip ...

  3. 【机制】js的闭包、执行上下文、作用域链

    1.从闭包说起 什么是闭包 一个函数和对其周围状态(词法环境)的引用捆绑在一起,这样的组合就是闭包. 也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域. 在 JavaScript 中,每 ...

  4. 【JavaScript】JS引擎中执行上下文如何顺序执行代码

    首先我们知道JavaScript引擎包括一个调用栈和堆,调用栈是代码实际执行的地方,使用执行上下文(执行环境)来完成:堆是非结构化的内存池,存储了应用程序所需要的所有对象. 执行上下文是什么? 执行上 ...

  5. 深入理解javascript作用域系列第五篇——一张图理解执行环境和作用域

    × 目录 [1]图示 [2]概念 [3]说明[4]总结 前面的话 对于执行环境(execution context)和作用域(scope)并不容易区分,甚至很多人认为它们就是一回事,只是高程和犀牛书关 ...

  6. JavaScript高级内容:原型链、继承、执行上下文、作用域链、闭包

    了解这些问题,我先一步步来看,先从基础说起,然后引出这些概念. 本文只用实例验证结果,并做简要说明,给大家增加些印象,因为单独一项拿出来都需要大篇幅讲解. 1.值类型 & 引用类型 funct ...

  7. JavaScript高级内容笔记:原型链、继承、执行上下文、作用域链、闭包

    最近在系统的学习JS深层次内容,并稍微整理了一下,作为备忘和后期复习,这里分享给大家,希望对大家有所帮助.如有错误请留言指正,tks. 了解这些问题,我先一步步来看,先从稍微浅显内容说起,然后引出这些 ...

  8. JavaScript:理解执行环境、作用域链和活动对象

    作用域的原理,对JS将如何解析标识符做出了解答.而作用域的形成与执行环境和活动对象紧密相关. 我们对于JS标识符解析的判断,存在一个常见误区 首先,看一个关于JS标识符解析的问题 ,源于风雪之隅提出的 ...

  9. Javascript本质第二篇:执行上下文

    在上一篇文章<Javascript本质第一篇:核心概念>中,对Javascript执行上下文做了解释,但是这些都是基于Javascript标准中对执行上下文的定义,也就是说理论上的东西,本 ...

  10. MySQL解析过程、执行过程

    转载:https://student-lp.iteye.com/blog/2152601 https://www.cnblogs.com/cdf-opensource-007/p/6502556.ht ...

随机推荐

  1. 【11个适合毕设的Python可视化大屏】用pyecharts开发拖拽式可视化数据大屏

    你好,我是@马哥python说,一枚10年程序猿. 一.效果演示 以下是我近期用Python开发的原创可视化数据分析大屏,非常适合毕设用,下面逐一展示:(以下是截图,实际上有动态交互效果哦) 以下大屏 ...

  2. 2.OS-Virtualization|抽象:进程

    进程:运行中的程序. 没有运行前,就是一个代码,他是静态的. 通过一系列抽象,有了状态. 通过进程抽象,我们需要明白计算机设计的一些思考点:如何分离?如何管理?如何抽象? 运行 就绪 阻塞 它就是一个 ...

  3. 2022-05-15:N个学校之间有单向的网络,每个学校得到一套软件后,可以通过单向网络向周边的学校传输。 问题1:初始至少需要向多少个学校发放软件,使得网络内所有的学校最终都能得到软件; 问题2:至

    2022-05-15:N个学校之间有单向的网络,每个学校得到一套软件后,可以通过单向网络向周边的学校传输. 问题1:初始至少需要向多少个学校发放软件,使得网络内所有的学校最终都能得到软件: 问题2:至 ...

  4. 2021-08-23:超级水王问题。扩展1:摩尔投票。扩展2:给定一个正数K,返回所有出现次数>N/K的数。

    2021-08-23:超级水王问题.扩展1:摩尔投票.扩展2:给定一个正数K,返回所有出现次数>N/K的数. 福大大 答案2021-08-23: 扩展1: 1.如果无候选,当前数就是候选,血为1 ...

  5. 啊哈C语言案例学习笔记

    Hello World #include<stdio.h> /* 技术要点: 初学者在编写程序时,经常会忘记在语句后边添加分号, */ int main() { printf(" ...

  6. Netty实战(二)

    一.环境准备 Netty需要的运行环境很简单,只有2个. JDK 1.8+ Apache Maven 3.3.9+ 二.Netty 客户端/服务器概览 如图,展示了一个我们将要编写的 Echo 客户端 ...

  7. 一分钟学一个 Linux 命令 - ls

    前言 大家好,我是 god23bin.今天我给大家带来的是 Linux 命令系列,每天只需一分钟,记住一个 Linux 命令不成问题.今天,我们要介绍的是一个常用而又强大的命令:ls(list). 什 ...

  8. 02. vmware搭建centos虚拟机并使用静态ip,局域网内可互通

    一.虚拟机镜像地址 我这里有镜像 二.目的 使用vmware搭建centos虚拟机集群,进行基础服务搭建,对系统业务提供服务支撑 三.效果 centos虚拟机ip不会自动改变,使用设置的静态ip,可以 ...

  9. 从RDD创建DataFrame

    0.前次作业:从文件创建DataFrame 1.pandas df 与 spark df的相互转换     df_s=spark.createDataFrame(df_p)     df_p=df_s ...

  10. 大数据实战手册-开发篇之RDD:计算 transform->action

    2.2 RDD:计算 transform->action 2.2.1 aggregate x = sc.parallelize([2,3,4], 2)[Task不能跨分片,task数为2] ne ...