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

 更新时间:2020年6月18日 07:24  点击:1784

壹 ❀ 引

我们都知道,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直接访问到它。

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

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之后由词法环境,变量环境来解释,两者概念不冲突,后者理解更为通俗易懂。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持猪先飞。

[!--infotagslink--]

相关文章

  • JavaScript判断浏览器及其版本信息

    本篇文章主要分享了通过window.navigator来判断浏览器及其版本信息的实例代码。具有一定的参考价值,下面跟着小编一起来看下吧...2017-01-23
  • js实现浏览器打印功能的示例代码

    这篇文章主要介绍了js如何实现浏览器打印功能,文中示例代码非常详细,帮助大家更好的理解和学习,感兴趣的朋友可以了解下...2020-07-15
  • 利用JS实现点击按钮后图片自动切换的简单方法

    下面小编就为大家带来一篇利用JS实现点击按钮后图片自动切换的简单方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2016-10-25
  • 详解前端安全之JavaScript防http劫持与XSS

    作为前端,一直以来都知道HTTP劫持与XSS跨站脚本、CSRF跨站请求伪造。防御这些劫持最好的方法是从后端入手,前端能做的太少。而且由于源码的暴露,攻击者很容易绕过防御手段。但这不代表我们去了解这块的相关知识是没意义的,本文的许多方法,用在其他方面也是大有作用。...2021-05-24
  • JavaScript仿支付宝密码输入框

    那么今天我就用JavaScript代码来实现这个效果吧,那么首先介绍一下整个的思路,首先我们先将确定输入密码的位数,我的需求是5位,那么就用一个div标签包住5个input标签...2016-01-02
  • js+css实现回到顶部按钮(back to top)

    这篇文章主要为大家详细介绍了js+css实现回到顶部按钮back to top回到顶部按钮,感兴趣的小伙伴们可以参考一下...2016-03-03
  • For循环中分号隔开的3部分的执行顺序探讨

    引发这个问题思考的是一段js程序的运行结果: 复制代码 代码如下: var i = 0; function a(){ for(i=0;i<20;i++){ } } function b(){ for(i=0;i<3;i++){ a(); } return i; } var Result = b(); 这段程序的运行结果是Re...2014-05-31
  • js实现上传图片及时预览

    这篇文章主要为大家详细介绍了js实现上传图片及时预览的相关资料,具有一定的参考价值,感兴趣的朋友可以参考一下...2016-05-09
  • 一个关于JS正则匹配的踩坑记录

    这篇文章主要给大家介绍了一个关于JS正则匹配的踩坑记录,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-04-13
  • 如何使用JavaScript实现无缝滚动自动播放轮播图效果

    这篇文章主要介绍了如何使用JavaScript实现“无缝滚动 自动播放”轮播图效果,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2020-08-20
  • Nest.js参数校验和自定义返回数据格式详解

    这篇文章主要给大家介绍了关于Nest.js参数校验和自定义返回数据格式的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-03-28
  • node.js如何操作MySQL数据库

    这篇文章主要介绍了node.js如何操作MySQL数据库,帮助大家更好的进行web开发,感兴趣的朋友可以了解下...2020-10-29
  • js屏蔽F12审查元素,禁止修改页面代码等实现代码

    有时候我们需要屏蔽客户端的F12,以防菜鸟也可以随意修改我们的代码,也处于源码的保护等操作,这里就为大家分享一下常见的代码...2020-10-03
  • 基于JavaScript实现文字超出部分隐藏

    这篇文章主要介绍了基于JavaScript实现文字超出部分隐藏 的相关资料,需要的朋友可以参考下...2016-03-01
  • js组件SlotMachine实现图片切换效果制作抽奖系统

    这篇文章主要介绍了js组件SlotMachine实现图片切换效果制作抽奖系统的相关资料,需要的朋友可以参考下...2016-04-19
  • js实现列表按字母排序

    这篇文章主要为大家详细介绍了js实现列表按字母排序,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2020-08-11
  • NodeJS实现阿里大鱼短信通知发送

    本文给大家介绍的是nodejs实现使用阿里大鱼短信API发送消息的方法和代码,有需要的小伙伴可以参考下。...2016-01-20
  • js实现文本框输入文字个数限制代码

    这篇文章主要介绍了js实现文本框输入文字个数限制代码,文本框输入的文字个数并不是无限制的,一般都会限定一个输入最高上限,如何限制,请看本文...2015-12-27
  • js实现调用网络摄像头及常见错误处理

    这篇文章主要介绍了js实现调用网络摄像头及常见错误处理,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-03-07
  • JS创建Tag标签的方法详解

    这篇文章主要介绍了JS创建Tag标签的方法,结合具体实例形式分析了javascript动态操作页面HTML元素实现tag标签功能的步骤与相关操作技巧,需要的朋友可以参考下...2017-06-15