必看参考:

请移步:博客园 JavaScript的执行上下文

深入理解JavaScript执行上下文和执行栈

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执行上下文和执行栈的更多相关文章

  1. 深入理解 JavaScript 执行上下文和执行栈

    前言 如果你是一名 JavaScript 开发者,或者想要成为一名 JavaScript 开发者,那么你必须知道 JavaScript 程序内部的执行机制.执行上下文和执行栈是 JavaScript ...

  2. 理解 Javascript 执行上下文和执行栈

    如果你是一名 JavaScript 开发者,或者想要成为一名 JavaScript 开发者,那么你必须知道 JavaScript 程序内部的执行机制.理解执行上下文和执行栈同样有助于理解其他的 Jav ...

  3. Javascript执行上下文和执行栈

    什么是执行上下文? 执行上下文就是当前JavaScript代码被解析和执行时所在环境的抽象概念,JavaScript中运行任何的代码都是在执行上下文. 什么是执行栈? 执行栈,在其他编程语言中也被叫做 ...

  4. JS高级学习笔记(6)- 事件循环

    参考文章:深入理解JS引擎的执行机制        JavaScript 异步.栈.事件循环.任务队列 我的笔记:ES系列之Promise async 和 await Event Loop 前提 js ...

  5. JS高级学习笔记(1)- 数据类型及转换规则

    必读: Javascript对象Oject的强制类型转换 JavaScript筑基篇(二)->JavaScript数据类型 聊一聊valueOf和toString 深入理解JavaScript系 ...

  6. JS高级学习笔记(9) 之 转:前端路由跳转基本原理

    原文链接: 前端路由跳转基本原理 前述 前端三大框架Angular.React和Vue都推行单页面应用SPA开发模式,这是因为在路由切换时,替换DOM Tree中发生修改的DOM部分,来减少原来因为多 ...

  7. JS高级学习笔记(10) 之 js 时怎么解析HTML标签的

    DOM 节点类型 浏览器渲染过程 浏览器是怎么把HTML标签语言和JavaScript联系在一起的,这就是我们常说的DOM. 浏览器中的DOM解析器把HTML翻译成对象(object),然后JavaS ...

  8. JS高级学习笔记(2)之js多线程

    参考大神:Javascript多线程 web worker ---- 6.Web Worker 概述 截图过来: 线程之间的通信 let worker = new Worker(‘js文件路径’) 主 ...

  9. 【学习笔记】JavaScript的基础学习

    [学习笔记]JavaScript的基础学习 一 变量 1 变量命名规则 Camel 标记法 首字母是小写的,接下来的字母都以大写字符开头.例如: var myTestValue = 0, mySeco ...

随机推荐

  1. Activemq、Rabbitmq、Rocketmq、Kafka的对比

    综上所述,各种对比之后,我个人倾向于是: 一般的业务系统要引入MQ,最早大家都用ActiveMQ,但是现在确实大家用的不多了,没经过大规模吞吐量场景的验证,社区也不是很活跃,所以大家还是算了吧,我个人 ...

  2. MyBatis-Insert、Delete、Update的注意事项

    MyBatis-Insert.Delete.Update的注意事项 插入/更新乱码的解决 出现插入乱码,首先要考虑数据库的编码集是不是UTF-8 如果数据库的编码无误,查看MyBatis的全局配置文件 ...

  3. java多线程知识回顾(笔记)

    线程创建的方式 有两种 第一种是继承Thread类 重写run方法 (个人偏向这一种实际中这种用的较多) 例如 public class MyThead extends Thread { int j= ...

  4. Lua生成比较理想的随机数的方法

    lua需要生成随机数的需求也是很常见的,为了生成看起来更随机的数字,我们需要注意以下几点 我们也需要给随机数设置随机数种子:math.randomseed(xx) lua对随机数种子也是有一定要求的: ...

  5. ffmpeg 学习:001-搭建开发环境

    介绍 由于命令行的ffmpeg工具无法满足产品的性能要求,需要对视频流进行兼容.所以需要调试有关的参数. FFmpeg全名是Fast Forward MPEG(Moving Picture Exper ...

  6. 201706 Ruby 基础 & 元编程

    yield yield self Proc yield带参数 rails中:yield 和 content_for methods.proc.lambda.block 闭包(用proc延长变量的生命周 ...

  7. python练习题4

    1.将字符串“老男孩”转换成UTF-8编码的字节类型 s = "老男孩" bytes(s,'utf8') s.encode('utf8') 2.简述globals(),locals ...

  8. 048、Java中使用switch判断

    01.代码如下: package TIANPAN; /** * 此处为文档注释 * * @author 田攀 微信382477247 */ public class TestDemo { public ...

  9. 043、Java中逻辑运算之实现位与操作

    01.代码如下: package TIANPAN; /** * 此处为文档注释 * * @author 田攀 微信382477247 */ public class TestDemo { public ...

  10. 六、Vue-Router:基础路由处理、路由提取成单独文件、路由嵌套、路由传参数、路由高亮、html5的history使用

    一.vue-router的安装 官网文档 [官网]:https://cn.vuejs.org/v2/guide/routing.html [router文档]:https://router.vuejs ...