

从javascript代码解析过程理解执行上下文与作用域提升
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.

从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)
以上代码经过如下步骤才被执行
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)栈顶弹出
所以上面代码执行结果为
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高级,还有很多需要开发者掌握的地方,可以看看我写的其他博文,持续更新中~
Recommend
-
81
-
62
视频: The Ultimate Guide to Execution Contexts, Hoisting, Scopes, and Closures...
-
31
这是一篇很短的文章,介绍了js几个比较重要的概念,适合通勤路上快速阅读加深理解和记忆。 作用域和执行上下文 作用域: js中的作用域是词法作用域,即由 函数声明时 所在的位置决定的。词法作用域是指在编译阶段就产生的,一整套函数标识符的访问规则。(区别于词...
-
60
1. 什么是作用域 作用域是你的代码在运行时,某些特定部分中的变量,函数和对象的可访问性。换句话说,作用域决定了变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。 2. JavaScript中的作用域 在 JavaScript 中有两种作用域
-
16
在上一篇文章 深入理解JavaScript 执行上下文 中提到 只有理解了执行上下文,才能更好地理解 JavaScript 语言本身,比如变量提升,作用域,闭包等,本篇文章就来说一下 JavaS...
-
9
原文网址:http://yanhaijing.com/javascript/2013/08/30/understanding-scope-and-context-in-javascript/ ...
-
8
前言 如果你是或者你想成为一名合格的前端开发工作者,你必须知道JavaScript代码在执行过程,知道执行上下文、作用域、变量提升等相关概念,并且熟练应用到自己的代码中。本文参考了你不知道的JavaScript,和JavaScript高级程序设计,以...
-
10
mini-spring 中文版 About The mini-spring is a simplified version of the Spring framework that will help you qui...
-
6
什么是V8?V8 是一个由 Google 开发的开源 JavaScript 引擎,目前用在 Chrome 浏览器和 Node.js 中,其核心功能是执行易于人类理解的 JavaScript 代码。V8 采用混合使用编译器和解释器的技术,称为 JIT(Just In Time)技术。下面是 V8...
-
29
Spring 手撸专栏 - 易学、好写、能懂! 小傅哥,一线互联网 Java 工程师、架构师,开发过交易、营销类项目,实现过运营、活动类项...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK