必看参考:

请移步:博客园 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. 图解IDEA中配置Maven并创建Maven的Web工程

    打开IDEA,File->Settings,如下图所示: 2.在Settings中按照如下进行配置,如下图所示:

  2. leetcode814 Binary Tree Pruning

    """ We are given the head node root of a binary tree, where additionally every node's ...

  3. 多元线性回归算法python实现(非常经典)

    对于多元线性回归算法,它对于数据集具有较好的可解释性,我们可以对比不过特征参数的输出系数的大小来判断它对数据的影响权重,进而对其中隐含的参数进行扩展和收集,提高整体训练数据的准确性.整体实现代码如下所 ...

  4. CSS-lineheight

    .test div{width:300px;margin:15px 0;border:1px solid #000;}.test p{margin:0;font-size:30px;}.fixed d ...

  5. NO31 配置网卡--主机名--网络故障排查面试题--DNS

    修改网卡配置信息: 修改主机名规范的三个步骤: 配置默认网关: DNS解析过程,用命令看:  DNS相关命令: 口述DNS解析过程: 客户端(电脑)通过浏览器输入域名,先找hosts文件及本地dns缓 ...

  6. 2.15 学习总结 之 天气预报APP volley(HTTP库)之StringRequest

    一.说在前面   昨天 学习了序列化的相关知识   今天 1.学习 volley(HTTP库)的 StringRequest请求 2.使用序列化完成相关案例 遇到问题 请求到的参数的出现中文乱码问题 ...

  7. GNS3 模拟DHCP之地址请求

    R1: conf t int f0/0 no shutdown ip address dhcp end R2: conf t int f0/0 no shutdown ip add 12.1.1.2 ...

  8. 微信小程序是什么

    官方的开发文档 微信小程序写的不多,随便写写 创建项目,分析工具 微信小程序有专门的编辑工具,去官网下载 然后申请一个小程序项目,获得一个appId,然后进入编辑工具就可以直接开发了 编辑工具可以设置 ...

  9. php的排序算法

    *对于算法来说,对于每个小伙伴来说都是比较头疼的,但是,为什么要学习算法? 算法是基础,算法能够提升智力,我想这两点就值得我们花时间去学习了.不要放弃,实在不会,先死记硬背下来,以后慢慢理解,一下是我 ...

  10. Window Server 2019 配置篇(2)- 在window server core上安装网络跟DHCP服务

    上一篇我们已经建立了自己的域服务器 之后我们将安装一个window server core,也就是没有GUI只有命令行的window server,并在其上安装网络服务和DHCP 首先创建一个新的虚拟 ...