首页 > 傻傻分不清的javascript运行机制

傻傻分不清的javascript运行机制

学习到javascript的运行机制时,有几个概念经常出现在各种文章中且容易混淆。Execution Context(执行环境或执行上下文),Context Stack (执行栈),Variable Object(VO: 变量对象),Active Object(AO: 活动对象),LexicalEnvironment(词法环境),VariableEnvironment(变量环境)等,特别是 VO,AO以及LexicalEnvironment,VariableEnvironment的区别很多文章都没有涉及到。因此我查看了一些国内外的文章,结合自身理解写下了下面的笔记。虽然因为自身不足导致理解上的偏差,但是依然相信读完下文会对理解javascript的一些概念如变量提升,作用域和闭包有很大的帮助。

一, 执行环境和执行栈

了解javascript的运行机制,首先必须掌握两个基本的概念。Execution Context(执行环境或执行上下文)和Context Stack (执行栈)

1. 何为执行环境(执行上下文)(Execution Context)

我们知道javascript是单线程语言,也就是同一时间只能执行一个任务。当javascript解释器初始化代码后,默认会进入全局的执行环境,之后每调用一个函数,javascript解释器会创建一个新的执行环境。

    var a = 1;                       // 1.初始化默认进入全局执行环境function b() {                   // 3.进入b 的执行环境function c() {               // 5. 进入c的执行环境···}c()                          // 4.在b的执行环境里调用c, 创建c的执行环境}b()                              // 2. 调用b 创建 b 的执行环境

执行环境的分类:

  • 全局执行环境:简单的理解,一个程序只有一个全局对象即window对象,全局对象所处的执行环境就是全局执行环境。
  • 函数执行环境:函数调用过程会创建函数的执行环境,因此每个程序可以有无数个函数执行环境。
  • Eval执行环境:eval代码特定的环境。

2. 如何单线程运行(Context Stack)

从一个简单的例子开始讲起

function foo(i) {if (i < 0) return;console.log('begin:' + i);foo(i - 1);console.log('end:' + i);
}
foo(2);

如何存储代码运行时的执行环境(全局执行环境,函数执行环境)呢,答案是执行栈。而栈遵循的是先进后出的原理,javascript初始化完代码后,首先会创建全局执行环境并推入当前的执行栈,当调用一个函数时,javascript引擎会创建新的执行环境并推到当前执行栈的顶端,在新的执行环境中,如果继续发生一个新函数调用时,则继续创建新的执行环境并推到当前执行栈的顶端,直到再无新函数调用。最上方的函数执行完成后,它的执行环境便从当前栈中弹出,并将控制权移交到当前执行栈的下一个执行环境,直到全局执行环境。当程序或浏览器关闭时,全局环境也将退出并销毁。

1513438-20181224134118896-1691306122.png

因此输出的结果为:

begin:2
begin:1
begin:0
end:0
end:1
end:2

3. 如何创建执行环境

我们现在知道每次调用函数时,javascript 引擎都会创建一个新的执行环境,而如何创建这一系列的执行环境呢,答案是执行器会分为两个阶段来完成, 分别是创建阶段和激活(执行)阶段。而即使步骤相同但是由于规范的不同,每个阶段执行的过程有很大的不同。

3.1 ES3 规范

创建阶段:

  • 1.创建作用域链。
  • 2.创建变量对象VO(包括参数,函数,变量)。
  • 3.确定this的值。

激活/执行阶段:

  • 完成变量分配,执行代码。
3.2 ES5 规范

创建阶段:

  • 1.确定 this 的值。
  • 2.创建词法环境(LexicalEnvironment)。
  • 3.创建变量环境(VariableEnvironment)。

激活/执行阶段:

  • 完成变量分配,执行代码。

我们从规范上可以知道,ES3和ES5在执行环境的创建阶段存在差异,当然他们都会在这个阶段确定this 的值

(关于this 的指向问题我们以后会在专门的文章中分析各种this 的指向问题,这里便不做深究)。我们将围绕这两个规范不同点展开。尽管ES3的一些规范已经被抛弃,但是掌握ES3 创建执行环境的过程依然有助于我们理解javascript深层次的概念。

二, Variable Object(VO: 变量对象),Active Object(AO: 活动对象)

2.1 基本概念

VO 和 AO 是ES3规范中的概念,我们知道在创建过程的第二个阶段会创建变量对象,也就是VO,它是用来存放执行环境中可被访问但是不能被 delete 的函数标识符,形参,变量声明等,这个对象在js环境下是不可访问的。而AO 和VO之间区别就是AO 是一个激活的VO,仅此而已。

  • 变量对象(Variable) object)是说JS的执行上下文中都有个对象用来存放执行上下文中可被访问但是不能被delete的函数标示符、形参、变量声明等。它们会被挂在这个对象上,对象的属性对应它们的名字对象属性的值对应它们的值但这个对象是规范上或者说是引擎实现上的不可在JS环境中访问到活动对象

  • 激活对象(Activation object)有了变量对象存每个上下文中的东西,但是它什么时候能被访问到呢?就是每进入一个执行上下文时,这个执行上下文儿中的变量对象就被激活,也就是该上下文中的函数标示符、形参、变量声明等就可以被访问到了

2.2 执行细节

如何创建VO对象可以大致分为四步

  • 1.创建arguments对象
  • 2.扫描上下文的函数声明(而非函数表达式),为发现的每一个函数,在变量对象上创建一个属性——确切的说是函数的名字——其有一个指向函数在内存中的引用。如果函数的名字已经存在,引用指针将被重写。
  • 3.扫描上下文的变量声明,为发现的每个变量声明,在变量对象上创建一个属性——就是变量的名字,并且将变量的值初始化为undefined。如果变量的名字已经在变量对象里存在,将不会进行任何操作并继续扫描。

注意: 整个过程可以大概描述成: 函数的形参=>函数声明=>变量声明, 其中在创建函数声明时,如果名字存在,则会被重写,在创建变量时,如果变量名存在,则忽略不会进行任何操作。

一个简单的例子

function foo(i) {var a = 'hello';var b = function privateB() {};function c() {}
}foo(22);

执行的伪代码

// 创建阶段
fooExecutionContext = {scopeChain: { ... },variableObject: {arguments: {0: 22,length: 1},i: 22,c: pointer to function c()a: undefined,b: undefined},this: { ... }
}
// 激活阶段
fooExecutionContext = {scopeChain: { ... },variableObject: {arguments: {0: 22,length: 1},i: 22,c: pointer to function c()a: 'hello',b: pointer to function privateB()},this: { ... }
}

三, LexicalEnvironment(词法环境),VariableEnvironment(变量环境)

3.1 基本概念

词法环境和变量环境是ES5以后提到的概念,官方对词法环境的解释如下。

词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符与特定变量和函数的关联关系。词法环境由环境记录(environment record)和可能为空引用(null)的外部词法环境组成。

简单的理解,词法环境是一个包含标识符变量映射的结构。(这里的标识符表示变量/函数的名称,变量是对实际对象【包括函数类型对象】或原始值的引用)。

ES3的VO,AO为什么可以被抛弃?个人认为有两个原因,第一个是在创建过程中所执行的创建作用域链和创建变量对象(VO)都可以在创建词法环境的过程中完成。第二个是针对es6中存储函数声明和变量(let 和 const)以及存储变量(var)的绑定,可以通过两个不同的过程(词法环境,变量环境)区分开来。

3.2 词法环境(lexicalEnvironment)

词法环境由两个部分组成

  • 环境记录(enviroment record),存储变量和函数声明
  • 对外部环境的引用(outer),可以通过它访问外部词法环境

对外部环境的引用关系到作用域链,之后再分析,我们先来看看环境记录的分类。

环境记录分两部分

  • 声明性环境记录(declarative environment records): 存储变量、函数和参数, 但是主要用于函数 、catch词法环境。

    注意:函数环境下会存储arguments的值。而详细的过程可以参考VO 的执行细节,基本大同小异
  • 对象环境记录(object environment records), 主要用于with 和全局的词法环境

伪代码如下

// 全局环境
GlobalExectionContext = {  
// 词法环境LexicalEnvironment: {  EnvironmentRecord: {  ···}outer:   }  
}
// 函数环境
FunctionExectionContext = {  
// 词法环境LexicalEnvironment: {  EnvironmentRecord: {  // 包含argument}outer:   }  
}

3.3 变量环境(objectEnvironment)

变量环境也是个词法环境,主要的区别在于lexicalEnviroment用于存储函数声明和变量( let 和 const )绑定,而ObjectEnviroment仅用于存储变量( var )绑定。

3.4 伪代码展示

ES5规范下的整个创建过程可以参考下方的伪代码

let a = 20;  
const b = 30;  
var c;function d(e, f) {  var g = 20;  return e * f * g;  
}c = d(20, 30);
// 全局环境
GlobalExectionContext = {this: ,// 词法环境LexicalEnvironment: {  EnvironmentRecord: {  Type: "Object",  // 环境记录分类: 对象环境记录a: < uninitialized >,  // 未初始化b: < uninitialized >,  d: < func >  }  outer:   },VariableEnvironment: {  EnvironmentRecord: {  Type: "Object",  // 环境记录分类: 对象环境记录c: undefined,  // undefined}  outer:   }  
}
// 函数环境
FunctionExectionContext = {  this: ,LexicalEnvironment: {  EnvironmentRecord: {  Type: "Declarative",  // 环境记录分类: 声明环境记录Arguments: {0: 20, 1: 30, length: 2},  // 函数环境下,环境记录比全局环境下的环境记录多了argument对象},  outer:   },VariableEnvironment: {  EnvironmentRecord: {  Type: "Declarative",  // 环境记录分类: 声明环境记录g: undefined  },  outer:   }  
}

四,作用域链

前面讲创建过程中,我们留下了一个伏笔,ES3规范中有创建作用域链的过程,而ES5中在创建词法环境或变量环境的过程中,也有生成外部环境的引用的过程。那这个过程有什么作用呢。我们通过一个简单的例子来说明。

function one() {var a = 1;two();function two() {var b = 2;three();function three() {var c = 3;alert(a + b + c); // 6}}}one();

当执行到three 的执行环境时,此时 a和b 都不在c 的变量内,因此作用域链则起到了引用外部执行环境变量的作用。ES3中创建的作用域链如图:

1513438-20181224134138844-1122856326.png

当解释器执行alert(a + b + c),他首先会找自身执行环境下是否有a这个变量的存在,如果不存在,则通过查看作用域链,判断a是否在上一个执行环境内部。它检查是否a存在于内部,若找不到,则沿着作用域链往上一个执行环境找,直到找到,或者到顶级的全局作用域。同理ES6规范中也可以这样分析。

因此这会引入一个javascript一个重要的概念,闭包。从上面对执行环境的解释我们可以这样理解,闭包就是内部环境通过作用域链访问到上层环境的变量。因此也存在无法进行变量回收的问题,只要函数的作用域链在,变量的值便因为闭包无法被回收。

注意: 此作用域链和原型链的作用域链不是同一个概念。

五, 小结

通过对javascript运行机制的介绍,对一些javasript高级概念有了更深的认识,特别是对一些云里雾里的概念区别有了更深刻的认识。不同规范下,不同概念的解释更有利于深挖javascript底层的执行思想。我相信这是理解javascipt语言最重要的一步。

参考资料:

  • https://stackoverflow.com/questions/20139050/what-really-is-a-declarative-environment-record-and-how-does-it-differ-from-an-a#
  • http://davidshariff.com/blog/what-is-the-execution-context-in-javascript/
  • http://davidshariff.com/blog/javascript-scope-chain-and-closures/
  • https://github.com/yued-fe/y-translation/blob/master/en/understanding-execution-context-and-execution-stack-in-javascript.md

本文为博主原创文章,转载请注明出处 https://juejin.im/post/5c20526b6fb9a049b7805ff9

转载于:https://www.cnblogs.com/kidflash/p/10168153.html

更多相关:

  • 第二章  配置开发环境...

  • 文章目录1. 编译前基本环境准备2. 编译 及 导入 Intellij IDEA3. Zookeeper 运行环境配置...

  • 查看环境变量 查看某一环境变量:比如我们需要查看HOME这个环境变量,我们可以在shell下直接输入echo $HOME 我们可以把所有的环境变量和环境变量的值都打印出来 打印环境变量 libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以 在使用时 要用extern声明...

  •  step1: 安装虚拟环境: sudo pip3 install virtualenv # 安装虚拟环境sudo pip3 install virtualenvwrapper # 安装虚拟环境扩展包# 编辑家目录下的 .bashrc 文件,在最下面添加下面三行代码   export WORKON_HOME=$HOME/...

  • 在某些情况下(例如通过网络访问数据),常常不希望程序卡住而占用太多时间以至于造成界面假死。在这时、我们可以通过Thread、Thread + Invoke(UI)或者是 delegate.BeginInvoke 来避免界面假死,但是这样做时,某些代码或者是某个方法的执行超时的时间还是无法操控的。那么我们又是否有一种比较通用的方法、来设...

  • 传参 如果程序执行的时候需要加入参数,如 ./sample aa bb 使用 gdb 的时候可以使用如下方式 gdb --args ./sample aa bb 执行 启动 gdb 之后,直接使用 r,就是 run 的意思,或者可以使用 b 加一个断点进行调试。处理信号 如果在使用的时候,遇到类似下面的报错 Threa...

  • 进程的图文形象表示 阮一峰–进程与线程的一个简单解释 多进程实质 现在,多核CPU已经非常普及了,但是,即使过去的单核CPU,也可以执行多任务。由于CPU执行代码都是顺序执行的,那么,单核CPU是怎么执行多任务的呢? 答案就是操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务...

  • redis 事物: Redis 事物的实现: 首先 wath监控键值 myKey开启批量执行 multi,执行命令入列,执行 exec 。如果监控的键值mykey 没有被修改过,则exec 中批量执行的命令成功,否则执行失败。无论执行成功与否,都会执行取消wath的执行  Redis multi 批量执行,是先把批量中的命令放入队列...

  • 一、准备工作: 1.登录服务器,切换到root用户(su - root,然后输入密码,按enter),进入根目录:cd / 2.进入要安装jdk的目录,自己可以创建一个java目录,执行命令如下: cd /usr/local/ mkdir java 二、下载安装包 1.打开官网下载界面:https://www.oracle.com/...

  • 多线程编程 mind-Mapping保存有一下导图的xmind文件,可直接获取 互斥变量 互斥对象 ptrhead相关接口 条件变量 future异步访问类 async类 promise类 package_task类...

  • 我们在实际开发的过程中,可能需要某些类的成员变量并不是针对每一个对象的,而是针对每一个类而言的,比如在银行中有一个利率数据,我们希望的是,当一个利率改变的时候,所有的对象都能够看到这个改变的数据,并利用它,而不是每一个对象都有一个利率成员变量。这个时候就设计到了静态成员变量。 一. 内存那些事 静态成员变量是存放在静态...

  • volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。下面举例说明。在DSP开发中,经常需要等待某个事件的触...

  •   引子     阅读以下代码,并尝试分析 代码解析 在主线程中,线程Id为1,为线程变量赋值 变量==d6ff开启一个新的task,此时线程Id为4,变量==d6ff,并调用Task1开启一个同步Task3,线程Id为1。变量==d6ff,修改值==f598此时第二步启动的Task1运行,线程Id为4,变量==d6ff,修改值=...

  • 操作系统基础    操作系统是协调、控制、管理计算机硬件资源与软件资源的控制程序 为什么要用操作系统?    1.操作系统可以把复杂的操作简化给用户使用或者应用程序  2.可以让应用程序对计算机硬件竞争变的有序  一套完整的计算机分为:操作系统、应用程序、计算机硬件 编程语言的分类   机械语言:使用二进制让计算机工作   优点:运行...