必看参考:

请移步:博客园 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. 内网其他服务器节点连接Mysql数据库很慢的解决方案

    一.概述 使用jdbc方式对数据进行同步时,由于设置了数据库登录超时时间是10s,结果发现有的服务器节点可以连接,有的服务器节点不能连接Mysql数据库.排查了好长原因,最后,自己写了一个jdbc的测 ...

  2. Hive的原生部署方式

    一.Hive的部署 1.官方文档 https://cwiki.apache.org/confluence/display/Hive/GettingStarted 2.前提条件 需要安装JDK1.7之上 ...

  3. 蓝牙 BLE 协议学习: 001-BLE协议栈整体架构

    背景 在深入BLE协议帧之前,我们先看一下BLE协议栈整体架构. 转载自:<深入浅出低功耗蓝牙(BLE)协议栈> 架构 如上图所述,要实现一个BLE应用,首先需要一个支持BLE射频的芯片, ...

  4. 超赞!苹果新一代iPad确定:外形大变样

    导读 除了iPhone.新MacBook Pro外,苹果还准备新款的入门版iPad,这么来看的话,他们要发布的新品真的是太多了. 据产业链最新消息称,苹果将在今年9月份更新入门版iPad,具体来说就是 ...

  5. nohup command 2>&1 & 的含义

    nohup command 2>&1 &的含义: nohup:no hang up,意思是不挂断.表示永久执行命令,哪怕当前终端已经退出登录. 并且命令前面添加nohup之后,会 ...

  6. matlab练习程序(快速搜索随机树RRT)

    RRT快速搜索随机树英文全称Rapid-exploration Random Tree,和PRM类似,也是一种路径规划算法. 和PRM类似,算法也需要随机撒点,不过不同的是,该算法不是全局随机撒点,而 ...

  7. 014-查看PHP的环境变量

    <?php print("你正在用文件的名字为: "); print(__FILE__); print(" <BR>\n"); print(& ...

  8. 项目启动报错:Communications link failure

    2017-12-29 10:43:19,776 ERROR [com.alibaba.druid.pool.DruidDataSource] - <init datasource error, ...

  9. 0106 springMVC REST风格

    markdown 印象笔记语法练习带快捷键的 加粗 快捷键 cmd+b 斜体 cmd+i 分割线 cmd+u 编号列表: cmd+shift+o 无编号列表 cmd+shift+u 待办事项 cmd+ ...

  10. mysql 存储引擎入门