转:JS高级学习笔记(8)- JavaScript执行上下文和执行栈
必看参考:
写在开头
入坑前端已经 13 个月了,不能再称自己为小白,那么现在就来学习一下 JS 的执行相关的知识。
自己吹过的牛皮,含着泪跪着也要实现它! 比如,先定一个小目标:成为高级前端。加油!
废话少说,进入正题
执行上下文
执行上下文(Execution context,EC)就是 JS 代码的执行环境,也称执行上下文环境。
在 JS 中有三种代码运行环境:
Clobal Code环境:JS代码默认的环境
Function Code环境:代码进入函数时的环境
Eval Code环境:使用eval()执行环境(不常用)
当 JavaScript 代码执行的时候,会进入不同的执行上下文,这些执行上下文就构成了一个执行上下文栈(Execution context stack,ECS)。
执行上下文栈
JavaScript 引擎创建了执行上下文栈(Execution Context Stack)来管理执行上下文。可以把执行上下文栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。
从上面的流程图,我们需要记住几个关键点:
JavaScript执行在单线程上,所有的代码都是排队执行。
一开始浏览器执行全局的代码时,首先创建全局的执行上下文,压入执行栈的顶部。
每当进入一个函数的执行就会创建函数的执行上下文,并且把它压入执行栈的顶部。当前函数执行完成后,当前函数的执行上下文出栈,并等待垃圾回收。
浏览器的JS引擎总是访问栈顶的执行上下文。
全局上下文只有唯一的一个,它在浏览器关闭时出栈。
执行上下文的生命周期
执行上下文的声明周期包括三个阶段:创建阶段-执行阶段-回收阶段
创建阶段
当函数被调用时,但是未执行任何其内部代码之前,会做以下三件事:
创建变量对象:首先初始化函数的参数arguments,提升函数声明和变量声明。
创建作用域链(Scope Chain):在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。作用域链本身包含变量对象。作用域链用于解析变量。当被要求解析变量时,JS始终从代码嵌套的最内层开始,如果最内层没有找到变量,就会跳到上一层父作用域中查找,直到找到该变量。
初始化this
JS在执行之前需要被解析,解析的时候会先创建一个全局执行环境,完成一系列变量提升,函数声明提升等操作;
当遇到函数和变量同名且都会被提升的情况,函数声明优先级比较高,因此变量声明会被函数声明所覆盖,但是可以重新赋值。
一个函数在执行之前,也会创建一个函数执行环境,和全局上下文差不多,但是会多出 this、argument和函数的参数。
this的值是在执行的时候才能确认,定义的时候不能确认!
执行阶段
设置变量的值、函数的引用,然后解释/执行代码
回收阶段
执行上下文出栈等待虚拟机回收执行上下文。
测试例子
var a = "global var";
function foo() {
console.log(a);
}
function outerFunc() {
var b = "var in outerFunc";
console.log(b);
function innerFunc() {
var c = "var in innerFunc";
console.log(c);
foo();
}
innerFunc();
}
outerFunc()
/* 执行结果:
* var in outerFunc
* var in innerFunc
* global var
*/
分析:代码首先进入全局执行上下文(Clobal Execution context) ,然后函数调用,依次进入 outerFunc、innerFunc 和 foo 执行上下文中,执行上下文就可以表示如下:
当JS代码执行的时候,第一个进入的总是默认的 全局执行上下文(Clobal Execution context),所以说他总是在 执行上下文栈(Execution context stack,ECS)最底层。
在每一个上下文中,都有三个重要的属性:变量对象(Variable object,VO)、作用域链(Scope chain) 和 this。除了这三个比较重要的属性,Execution Context还可以有一些附加属性。
变量对象 和 活动对象
变量对象
从上面的例子中看,在执行上下文中,会保存变量对象(Variable Object,VO)变量对象是执行上下文相关的数据作用域。它是一个与上下文相关的特殊对象,其中存储了在上下文中定义的变量和函数声明。也就是说,一般变量对象中会包含以下信息:
变量(var, Variable Declaration)
函数声明(Function Declaration,FD)
函数的形参
当JS代码运行中,如果试图寻找一个变量的时候,就会首先查找VO。对于例子一种的代码,Global Execution Context中的VO如下:
注意,如果在上面例子中添加有下面语句,Global VO不会变化,这两句属于window中的变量。
(function bar(){})
baz = "property of global object"
也就是说,对于VO,是有两种特殊情况的:
函数表达式(与函数声明相对)不包含在VO之中
没有使用
var
声明的变量(这种变量是,“全局”的声明方式,只是给Global添加一个属性,并不在VO中)
活动对象
只有全局上下文的变量对象允许通过VO的属性名称间接访问;在函数执行上下文中,VO是不能直接访问的,此时有活动对象(Activation object)扮演VO的角色。活动对象 是在进入函数上下文时刻被创建,它通过函数的arguments属性初始化。
Argument Object是函数上下文里的激活对象AO中的内部对象,它包括下列属性:
callee:指向当前函数的引用
length:真正传递的参数的个数
properties-indexes:就是函数的参数值(按照参数列表从作到右排列)
对于VO和AO的关系可以理解为:VO在不同的Execution Context中会有不同的表现:当在Global Execution Context中,可以直接使用VO,但是在函数Execution Context中,AO就会被创建。
上面的例子开始执行outerFunc的时候,会有一个outerFunc的 活动对象 被创建:
接下来需要学习JS解释器是怎么执行这一段代码的,以及怎么设置VO和AO的。
创建VO/AO的一些细节
当一段JS代码执行时,JS解释器会创建Execution Context,其实这里会有两个阶段:
创建阶段(当函数被调用时,但是开始执行内部代码之前)
创建 Scope Chain
创建VO/AO
设置 this 的值
激活/代码执行阶段--就是执行阶段
设置变量的值、函数的引用,然后解释/执行代码
这里详细介绍一下“创建VO/AO”中的一些细节,因为这些内容将直接影响代码的运行行为。
第一步,根据函数的参数,创建并初始化arguments object
第二步,扫描函数内部的代码,查找函数声明(Function declaration)
对于所有找到的函数声明,将函数名和函数引用存入VO/AO中
如果VO/AO有同名的函数,那么就进行覆盖
第三步,扫描函数内部代码,查找变量声明(Variable declaration)
对于所有找到的变量声明,将变量名存入VO/AO中,并初始化为"undefined"
如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性
用下面的例子来认识“创建VO/AO”的细节
function foo(i) {
var a = 'hello';
var b = function privateB() { };
function c() { }
} foo(22);
对于上面的代码,在“创建阶段”,可以得到下面的Execution Context object:
fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: undefined,
b: undefined
},
this: { ... }
}
在"激活/代码执行阶段",Execution Context object就被更新为:
fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: 'hello',
b: pointer to function privateB()
},
this: { ... }
}
例子分析
Example 1
(function(){
console.log(bar);
console.log(baz); var bar = 20; function baz(){
console.log("baz");
} })()
在Chrome中运行代码运行后将输出:
代码解释:匿名函数会进入“创建结果”,JS解释器会创建一个"Function Execution Context",然后创建Scope chain,VO/AO和this。根据前面的介绍,解释器会扫描函数和变量声明,如下的AO会被创建:
所以,对于bar,我们会得到"undefined"这个输出,表现的行为就是,我们在声明一个变量之前就访问了这个变量。这个就是JavaScript中"Hoisting(提升)"。
Example 2
接着对上面的例子,进行一些修改:
(function(){
console.log(bar);
console.log(baz); bar = 20;
console.log(window.bar);
console.log(bar); function baz(){
console.log("baz");
} })()
运行这段代码会得到"bar is not defined(…)"错误。当代码执行到console.log(bar)
的时候,会去AO中查找"bar"。但是,根据前面的解释,自调用函数中的"bar"并没有通过var关键字声明,所有不会被存放在AO中,也就有了这个错误。 因为在创建阶段是扫描函数内部的代码,而bar = 20;
不是函数内部的代码。
注释掉"console.log(bar);",再次运行代码,可以得到下面结果。"bar"在"激活/代码执行阶段"被创建。
Example 3
(function(){
console.log(foo); // undefined
console.log(bar); // func...
console.log(baz); // func... var foo = function(){}; function bar(){
console.log("bar");
} var bar = 20;
console.log(bar); // function baz(){
console.log("baz");
} })()
代码的运行结果为:
代码中,最"奇怪"的地方应该就是"bar"的输出了,第一次是一个函数,第二次是"20"。
其实也很好解释,回到前面对"创建VO/AO"的介绍,在创建VO/AO过程中,解释器会先扫描函数声明,然后"foo: <function>"就被保存在了AO中;但解释器扫描变量声明的时候,虽然发现"var bar = 20;",但是因为"foo"在AO中已经存在,所以就没有任何操作了。
但是,当代码执行到第二句"console.log(bar);"的时候,"激活/代码执行阶段"已经把AO中的"bar"重新设置了。
总结
通过对VO/AO在"创建阶段"的具体细节,如何扫描函数声明和变量声明,就可以对JavaScript中的"Hoisting"有清晰的认识。
所以说,了解JavaScript解释器的行为,以及相关的概念,对理解JavaScript代码的行为是很有帮助的。
转:JS高级学习笔记(8)- JavaScript执行上下文和执行栈的更多相关文章
- 深入理解 JavaScript 执行上下文和执行栈
前言 如果你是一名 JavaScript 开发者,或者想要成为一名 JavaScript 开发者,那么你必须知道 JavaScript 程序内部的执行机制.执行上下文和执行栈是 JavaScript ...
- 理解 Javascript 执行上下文和执行栈
如果你是一名 JavaScript 开发者,或者想要成为一名 JavaScript 开发者,那么你必须知道 JavaScript 程序内部的执行机制.理解执行上下文和执行栈同样有助于理解其他的 Jav ...
- Javascript执行上下文和执行栈
什么是执行上下文? 执行上下文就是当前JavaScript代码被解析和执行时所在环境的抽象概念,JavaScript中运行任何的代码都是在执行上下文. 什么是执行栈? 执行栈,在其他编程语言中也被叫做 ...
- JS高级学习笔记(6)- 事件循环
参考文章:深入理解JS引擎的执行机制 JavaScript 异步.栈.事件循环.任务队列 我的笔记:ES系列之Promise async 和 await Event Loop 前提 js ...
- JS高级学习笔记(1)- 数据类型及转换规则
必读: Javascript对象Oject的强制类型转换 JavaScript筑基篇(二)->JavaScript数据类型 聊一聊valueOf和toString 深入理解JavaScript系 ...
- JS高级学习笔记(9) 之 转:前端路由跳转基本原理
原文链接: 前端路由跳转基本原理 前述 前端三大框架Angular.React和Vue都推行单页面应用SPA开发模式,这是因为在路由切换时,替换DOM Tree中发生修改的DOM部分,来减少原来因为多 ...
- JS高级学习笔记(10) 之 js 时怎么解析HTML标签的
DOM 节点类型 浏览器渲染过程 浏览器是怎么把HTML标签语言和JavaScript联系在一起的,这就是我们常说的DOM. 浏览器中的DOM解析器把HTML翻译成对象(object),然后JavaS ...
- JS高级学习笔记(2)之js多线程
参考大神:Javascript多线程 web worker ---- 6.Web Worker 概述 截图过来: 线程之间的通信 let worker = new Worker(‘js文件路径’) 主 ...
- 【学习笔记】JavaScript的基础学习
[学习笔记]JavaScript的基础学习 一 变量 1 变量命名规则 Camel 标记法 首字母是小写的,接下来的字母都以大写字符开头.例如: var myTestValue = 0, mySeco ...
随机推荐
- Python 实现远程服务器批量执行命令
paramiko 远程控制介绍 Python paramiko是一个相当好用的远程登录模块,采用ssh协议,可以实现linux服务器的ssh远程登录.首先来看一个简单的例子 import parami ...
- 使用vim编译.cpp文件
一.编写代码 1.打开命令行终端,输入vim test.cpp,新建了一个文件叫做“test.cpp”:如果以前已经建立过这个文件,则是打开这个名字的文件. 2.按回车进入编辑界面,输入i进入编辑模式 ...
- @Autowired 和 @ Resource 的区别
转 都是用来装配Bean的注解.都可以写在字段上,或写在setter方法上. @Autowired注解是按照类型(byType)装配依赖对象,默认情况下它要求依赖对象必须存在,如果允许null值,可以 ...
- Java常考面试题(二)(转)
序言 昨天刚开始的"每日5题面试"这类文章,感觉还不错,把一些平常看似懂了的东西,弄清楚了.就像什么是虚拟机?这个问题,看起来知道,但是要说出个所以然来,又懵逼了,经常回过头来看看 ...
- MinGW下编译curl-7.60.0时, 发生ERROR_FILE_NOT_FOUND undeclared
在编译curl-7.60.0时, 遇到ERROR_FILE_NOT_FOUND undeclared 这个情况, 就没法编译成功!! 下载了以往的版本, 发现是从curl-7.59.0版本开始才有 t ...
- JAVA String类常用方法
一.String类String类在java.lang包中,java使用String类创建一个字符串变量,字符串变量属于对象.java把String类声明的final类,不能有类.String类对象创建 ...
- 基于springboot实现Java阿里短信发送
1.接口TestController import java.util.Random; import com.aliyuncs.DefaultAcsClient; import com.aliyunc ...
- 福州大学2020年春软工实践W班第二次作业
作业描述 这个作业属于哪个课程 福州大学2020年春软工实践W班 这个作业要求在哪里 寒假作业(2/2) 这个作业的目标 开发一个疫情统计程序 作业正文 福州大学2020年春软工实践W班第二次作业 其 ...
- JavaScript原生Ajax请求纯文本数据
源代码 ajax1.html: <!DOCTYPE html> <html> <head> <meta charset="utf-8"&g ...
- 自己安装windows版本的Flink
参照 https://blog.csdn.net/clj198606061111/article/details/99694033 我自己做一遍 找到对应的网址 https://flink.apach ...