通过实例了解JS执行上下文运行原理


Posted in Javascript onJune 17, 2020

壹 ❀ 引

我们都知道,JS代码的执行顺序总是与代码先后顺序有所差异,当先抛开异步问题你会发现就算是同步代码,它的执行也与你的预期不一致,比如:

function f1() {
  console.log('听风是风');
};
f1(); //echo
function f1() {
  console.log('echo');
};
f1(); //echo

按照代码书写顺序,应该先输出 听风是风,再输出 echo才对,很遗憾,两次输出均为 echo;如果我们将上述代码中的函数声明改为函数表达式,结果又不太一样:

var f1 = function () {
  console.log('听风是风');
};
f1(); //听风是风
var f1 = function() {
  console.log('echo');
};
f1(); //echo

这说明代码在执行前一定发生了某些微妙的变化,JS引擎究竟做了什么呢?这就不得不提JS执行上下文的了。

贰 ❀ JS执行上下文

JS代码在执行前,JS引擎总要做一番准备工作,这份工作其实就是创建对应的执行上下文;

执行上下文有且只有三类,全局执行上下文,函数上下文,与eval上下文;由于eval一般不会使用,这里不做讨论。

1.全局执行上下文

全局执行上下文只有一个,在客户端中一般由浏览器创建,也就是我们熟知的window对象,我们能通过this直接访问到它。

通过实例了解JS执行上下文运行原理

全局对象window上预定义了大量的方法和属性,我们在全局环境的任意处都能直接访问这些属性方法,同时window对象还是var声明的全局变量的载体。我们通过var创建的全局对象,都可以通过window直接访问。

通过实例了解JS执行上下文运行原理

2.函数执行上下文

函数执行上下文可存在无数个,每当一个函数被调用时都会创建一个函数上下文;需要注意的是,同一个函数被多次调用,都会创建一个新的上下文。

说到这你是否会想,上下文种类不同,而且创建的数量还这么多,它们之间的关系是怎么样的,又是谁来管理这些上下文呢,这就不得不说说执行上下文栈了。

叁 ❀ 执行上下文栈(执行栈)

执行上下文栈(下文简称执行栈)也叫调用栈,执行栈用于存储代码执行期间创建的所有上下文,具有LIFO(Last In First Out后进先出,也就是先进后出)的特性。

JS代码首次运行,都会先创建一个全局执行上下文并压入到执行栈中,之后每当有函数被调用,都会创建一个新的函数执行上下文并压入栈内;由于执行栈LIFO的特性,所以可以理解为,JS代码执行完毕前在执行栈底部永远有个全局执行上下文。

function f1() {
  f2();
  console.log(1);
};
function f2() {
  f3();
  console.log(2);
};
function f3() {
  console.log(3);
};
f1();//3 2 1

我们通过执行栈与上下文的关系来解释上述代码的执行过程,为了方便理解,我们假象执行栈是一个数组,在代码执行初期一定会创建全局执行上下文并压入栈,因此过程大致如下:

//代码执行前创建全局执行上下文
ECStack = [globalContext];
// f1调用
ECStack.push('f1 functionContext');
// f1又调用了f2,f2执行完毕之前无法console 1
ECStack.push('f2 functionContext');
// f2又调用了f3,f3执行完毕之前无法console 2
ECStack.push('f3 functionContext');
// f3执行完毕,输出3并出栈
ECStack.pop();
// f2执行完毕,输出2并出栈
ECStack.pop();
// f1执行完毕,输出1并出栈
ECStack.pop();
// 此时执行栈中只剩下一个全局执行上下文

那么到这里,我们解释了执行栈与执行上下文的存储规则;还记得我在前文提到代码执行前JS引擎会做准备创建执行上下文吗,具体怎么创建呢,我们接着说。

肆 ❀ 执行上下文创建阶段

执行上下文创建分为创建阶段与执行阶段两个阶段,较为难理解应该是创建阶段,我们先说创建阶段。

JS执行上下文的创建阶段主要负责三件事:确定this---创建词法环境组件(LexicalEnvironment)---创建变量环境组件(VariableEnvironment)

这里我就直接借鉴了他人翻译资料的伪代码,来表示这个创建过程:

ExecutionContext = { 
  // 确定this的值
  ThisBinding = <this value>,
  // 创建词法环境组件
  LexicalEnvironment = {},
  // 创建变量环境组件
  VariableEnvironment = {},
};

如果你有阅读其它关于执行上下文的文章读到这里一定有疑问,执行上下文创建过程不是应该解释this,作用域与变量对象/活动对象才对吗,怎么跟别的地方说的不一样,这点我后面解释。

1.确定this

官方的称呼为This Binding,在全局执行上下文中,this总是指向全局对象,例如浏览器环境下this指向window对象。

而在函数执行上下文中,this的值取决于函数的调用方式,如果被一个对象调用,那么this指向这个对象。否则this一般指向全局对象window或者undefined(严格模式)。

2.词法环境组件

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

词法环境由环境记录与对外部环境引入记录两个部分组成。

其中环境记录用于存储当前环境中的变量和函数声明的实际位置;外部环境引入记录很好理解,它用于保存自身环境可以访问的其它外部环境,那么说到这个,是不是有点作用域链的意思?

我们在前文提到了全局执行上下文与函数执行上下文,所以这也导致了词法环境分为全局词法环境与函数词法环境两种。

全局词法环境组件:

对外部环境的引入记录为null,因为它本身就是最外层环境,除此之外它还记录了当前环境下的所有属性、方法位置。

函数词法环境组件:

包含了用户在函数中定义的所有属性方法外,还包含了一个arguments对象。函数词法环境的外部环境引入可以是全局环境,也可以是其它函数环境,这个根据实际代码而来。

这里借用译文中的伪代码(环境记录在全局和函数中也不同,全局中的环境记录叫对象环境记录,函数中环境记录叫声明性环境记录,说多了糊涂,下方有展示):

// 全局环境
GlobalExectionContext = {
  // 全局词法环境
  LexicalEnvironment: {
    // 环境记录
    EnvironmentRecord: {
      Type: "Object", //类型为对象环境记录
      // 标识符绑定在这里 
    },
    outer: < null >
  }
};
// 函数环境
FunctionExectionContext = {
  // 函数词法环境
  LexicalEnvironment: {
    // 环境纪录
    EnvironmentRecord: {
      Type: "Declarative", //类型为声明性环境记录
      // 标识符绑定在这里 
    },
    outer: < Global or outerfunction environment reference >
  }
};

3.变量环境组件

变量环境可以说也是词法环境,它具备词法环境所有属性,一样有环境记录与外部环境引入。在ES6中唯一的区别在于词法环境用于存储函数声明与let const声明的变量,而变量环境仅仅存储var声明的变量。

我们通过一串伪代码来理解它们:

let a = 20; 
const b = 30; 
var c;

function multiply(e, f) { 
 var g = 20; 
 return e * f * g; 
}

c = multiply(20, 30);

我们用伪代码来描述上述代码中执行上下文的创建过程:

//全局执行上下文
GlobalExectionContext = {
  // this绑定为全局对象
  ThisBinding: <Global Object>,
  // 词法环境
  LexicalEnvironment: { 
    //环境记录
   EnvironmentRecord: { 
    Type: "Object", // 对象环境记录
    // 标识符绑定在这里 let const创建的变量a b在这
    a: < uninitialized >, 
    b: < uninitialized >, 
    multiply: < func > 
   }
   // 全局环境外部环境引入为null
   outer: <null> 
  },
 
  VariableEnvironment: { 
   EnvironmentRecord: { 
    Type: "Object", // 对象环境记录
    // 标识符绑定在这里 var创建的c在这
    c: undefined, 
   }
   // 全局环境外部环境引入为null
   outer: <null> 
  } 
 }

 // 函数执行上下文
 FunctionExectionContext = {
   //由于函数是默认调用 this绑定同样是全局对象
  ThisBinding: <Global Object>,
  // 词法环境
  LexicalEnvironment: { 
   EnvironmentRecord: { 
    Type: "Declarative", // 声明性环境记录
    // 标识符绑定在这里 arguments对象在这
    Arguments: {0: 20, 1: 30, length: 2}, 
   }, 
   // 外部环境引入记录为</Global>
   outer: <GlobalEnvironment> 
  },
 
  VariableEnvironment: { 
   EnvironmentRecord: { 
    Type: "Declarative", // 声明性环境记录
    // 标识符绑定在这里 var创建的g在这
    g: undefined 
   }, 
   // 外部环境引入记录为</Global>
   outer: <GlobalEnvironment> 
  } 
 }

不知道你有没有发现,在执行上下文创建阶段,函数声明与var声明的变量在创建阶段已经被赋予了一个值,var声明被设置为了undefined,函数被设置为了自身函数,而let const被设置为未初始化。

现在你总知道变量提升与函数声明提前是怎么回事了吧,以及为什么let const为什么有暂时性死域,这是因为作用域创建阶段JS引擎对两者初始化赋值不同。

上下文除了创建阶段外,还有执行阶段,这点大家应该好理解,代码执行时根据之前的环境记录对应赋值,比如早期var在创建阶段为undefined,如果有值就对应赋值,像let const值为未初始化,如果有值就赋值,无值则赋予undefined。

伍 ❀ 关于变量对象与活动对象

回答前面的问题,为什么别人的博文介绍上下文都是谈作用域,变量对象和活动对象,我这就成了词法环境,变量环境了。

我在阅读相关资料也产生了这个疑问,一番查阅可以确定的是,变量对象与活动对象的概念是ES3提出的老概念,从ES5开始就用词法环境和变量环境替代了,因为更好解释。

在上文中,我们通过介绍词法环境与变量环境解释了为什么var会存在变量提升,为什么let const没有,而通过变量对象与活动对象是很难解释的,由其是在JavaScript在更新中不断在弥补当初设计的坑。

其次,词法环境的概念与变量对象这类概念也是可以对应上的。

我们知道变量对象与活动对象其实都是变量对象,变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。而在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。

那这不正好对应到了全局词法记录与函数词法记录了吗。而且由于ES6新增的let const不存在变量提升,于是正好有了词法环境与变量环境的概念来解释这个问题。

所以说到这,你也不用为词法环境,变量对象的概念闹冲突了。

我们来总结下上面提到的概念。

陆 ❀ 总结

1.全局执行上下文一般由浏览器创建,代码执行时就会创建;函数执行上下文只有函数被调用时才会创建,调用多少次函数就会创建多少上下文。

2.调用栈用于存放所有执行上下文,满足FILO规则。

3.执行上下文创建阶段分为绑定this,创建词法环境,变量环境三步,两者区别在于词法环境存放函数声明与const let声明的变量,而变量环境只存储var声明的变量。

4.词法环境主要由环境记录与外部环境引入记录两个部分组成,全局上下文与函数上下文的外部环境引入记录不一样,全局为null,函数为全局环境或者其它函数环境。环境记录也不一样,全局叫对象环境记录,函数叫声明性环境记录。

5.你应该明白了为什么会存在变量提升,函数提升,而let const没有。

6.ES3之前的变量对象与活动对象的概念在ES5之后由词法环境,变量环境来解释,两者概念不冲突,后者理解更为通俗易懂。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
asp.net HttpHandler实现图片防盗链
Nov 09 Javascript
javascript 静态对象和构造函数的使用和公私问题
Mar 02 Javascript
javascript实现的多个层切换效果通用函数实例
Jul 06 Javascript
jQuery插件formValidator实现表单验证
May 23 Javascript
jQuery Ajax 加载数据时异步显示加载动画
Aug 01 Javascript
angularjs指令之绑定策略(@、=、&amp;)
Apr 13 Javascript
浅谈js闭包理解
Apr 01 Javascript
常见的浏览器存储方式(cookie、localStorage、sessionStorage)
May 07 Javascript
Vue使用自定义指令实现拖拽行为实例分析
Jun 06 Javascript
解决removeEventListener 无法清除监听的问题
Oct 30 Javascript
微信小程序实现底部弹出框
Nov 18 Javascript
js 数组 fill() 填充方法
Nov 02 Javascript
从0搭建vue-cli4脚手架
Jun 17 #Javascript
微信小程序点击生成朋友圈分享图(遇到的坑)
Jun 17 #Javascript
基于JS+HTML实现弹窗提示是否确认提交功能
Jun 17 #Javascript
vue移动端的左右滑动事件详解
Jun 17 #Javascript
详解JavaScript中的Object.is()与&quot;===&quot;运算符总结
Jun 17 #Javascript
vue-iview动态新增和删除的方法
Jun 17 #Javascript
vue iview实现动态新增和删除
Jun 17 #Javascript
You might like
php与flash as3 socket通信传送文件实现代码
2014/08/16 PHP
php实现获取文章内容第一张图片的方法
2014/11/04 PHP
linux下为php添加iconv模块的方法
2016/02/28 PHP
yii2 页面底部加载css和js的技巧
2016/04/21 PHP
javascript 伪数组实现方法
2010/10/11 Javascript
Javascript面向对象编程(二) 构造函数的继承
2011/08/28 Javascript
JavaScript中消除闭包的一般方法介绍
2015/03/16 Javascript
JavaScript中Number.NEGATIVE_INFINITY值的使用详解
2015/06/05 Javascript
jquery无限级联下拉菜单简单实例演示
2015/11/23 Javascript
利用JS判断鼠标移入元素的方向
2016/12/11 Javascript
webpack中CommonsChunkPlugin详细教程(小结)
2017/11/09 Javascript
vue中子组件向父组件传递数据的实例代码(实现加减功能)
2018/04/20 Javascript
手把手教你使用TypeScript开发Node.js应用
2019/05/06 Javascript
js图片无缝滚动插件使用详解
2020/05/26 Javascript
关于ckeditor在bootstrap中modal中弹框无法输入的解决方法
2019/09/11 Javascript
Vue.js 中制作自定义选择组件的代码附演示demo
2020/02/28 Javascript
[01:17:55]VGJ.T vs Mineski 2018国际邀请赛小组赛BO2 第一场 8.18
2018/08/20 DOTA
Python 条件判断的缩写方法
2008/09/06 Python
python3.4用函数操作mysql5.7数据库
2017/06/23 Python
Bottle框架中的装饰器类和描述符应用详解
2017/10/28 Python
Python3实现的字典遍历操作详解
2018/04/18 Python
python分批定量读取文件内容,输出到不同文件中的方法
2018/12/08 Python
浅谈如何使用python抓取网页中的动态数据实现
2020/08/17 Python
英国健康和美容技术产品购物网站:CurrentBody
2019/07/17 全球购物
英国领先的男装设计师服装独立零售商:Repertoire Fashion
2020/10/19 全球购物
委托与事件是什么关系?为什么要使用委托
2014/04/18 面试题
白酒业务员岗位职责
2013/12/27 职场文书
公司成立感言
2014/01/11 职场文书
公立医院改革实施方案
2014/03/14 职场文书
学生安全承诺书
2014/05/22 职场文书
2014年秋季新学期寄语
2014/08/02 职场文书
建议书格式
2015/02/04 职场文书
中国世界遗产导游词
2015/02/13 职场文书
2015年12.4全国法制宣传日活动总结
2015/03/24 职场文书
预防职务犯罪警示教育心得体会
2016/01/15 职场文书
一次项目中Thinkphp绕过禁用函数的实战记录
2021/11/17 PHP