JavaScript 中的执行上下文和执行栈实例讲解


Posted in Javascript onFebruary 25, 2021

JavaScript - 原理系列

​ 在日常开发中,每当我们接手一个现有项目后,我们总喜欢先去看看别人写的代码。每当我们看到别人写出很酷的代码的时候,我们总会感慨!写出这么优美而又简洁的代码的兄弟到底是怎么养成的呢?

​ 我要怎样才能达到和大佬一样的水平呢!好了,废话不多说,让我们切入今天的主题。

一、执行上下文

​ 简而言之,【执行上下文】就是JavaScript 代码被解析和执行时所在环境的抽象概念, 在JavaScript 中运行任何的代码都是在它的执行上下文中运行。

​ 在运行JavaScript代码时,每当需要执行代码时,执行代码会先进入一个环境(浏览器、Node客户端),这时就会为该环境创建一个执行上下文,它会在你运行代码前做一些准备工作,如确定作用域,创建全局、局部变量对象等。

执行上下文的分类

  • 全局执行上下文:

​ 这是默认的、最基础的执行上下文。不在任何函数中的代码都位于全局执行上下文中。

它做了两件事:

  • 创建一个全局对象,在浏览器中这个全局对象就是 window 对象。

this 指针指向这个全局对象。一个程序中只能存在一个全局执行上下文。

  • 函数执行上下文:

​ 每次调用函数时,都会为该函数创建一个新的执行上下文。每个函数都拥有自己的执行上下文,但是只有在函数被调用的时候才会被创建。一个程序中可以存在任意数量的函数执行上下文。每当一个新的执行上下文被创建,它都会按照特定的顺序执行一系列步骤,具体过程将在本文后面讨论。

  • Eval 函数执行上下文:

​ 运行在 eval 函数中的代码也获得了自己的执行上下文,但由于 Javascript 开发人员不常用 eval 函数,所以在这里不再讨论。

执行上下文的数量限制(堆栈溢出)

​执行上下文可存在多个,虽然没有明确的数量限制,但如果超出栈分配的空间,会造成堆栈溢出。常见于递归调用,没有终止条件造成死循环的场景。

下面是示例代码:

// 递归调用自身
function foo() {
  foo();
}
foo();
// 报错:Uncaught RangeError: Maximum call stack size exceeded

Tips:

​ JS是“单线程”的,每次只执行一段代码

二、执行栈

​ JS中的执行栈,也就是在其它编程语言中所说的“调用栈”,是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。

​ 当 JavaScript 引擎第一次遇到你的脚本时,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。

​ 引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。

栈数据结构

JavaScript 中的执行上下文和执行栈实例讲解

现在让我们用一段代码来理解执行栈

let a = 'Hello World!';

function first() {
 console.log('Inside first function');
 second();
 console.log('Again inside first function');
}

function second() {
 console.log('Inside second function');
}

first();
console.log('Inside Global Execution Context');

下图是上面代码的执行栈

JavaScript 中的执行上下文和执行栈实例讲解

​ 当上述代码在浏览器加载时,浏览器的JavaScript 引擎会创建一个全局执行上下文并把它压入当前执行栈。当遇到函数调用时,JavaScript 引擎为该函数创建一个新的执行上下文并把它压入当前执行栈的顶部。

当从 first()函数内部调用 second()函数时,JavaScript 引擎为 second() 函数创建了一个新的执行上下文并把它压入当前执行栈的顶部。当 second()函数执行完毕,它的执行上下文会从当前栈弹出,并且控制流程到达下一个执行上下文,即 first() 函数的执行上下文。

​ 当 first() 执行完毕,它的执行上下文从栈弹出,控制流程到达全局执行上下文。一旦所有代码执行完毕,JavaScript 引擎从当前栈中移除全局执行上下文。

The Creation Phase

​在 JavaScript 代码执行前,执行上下文将经历创建阶段。在创建阶段会发生三件事:

  1. this 值的决定,即我们所熟知的 This 绑定。
  2. 创建词法环境组件。
  3. 创建变量环境组件。

所以执行上下文在概念上表示如下:

ExecutionContext = {
 ThisBinding = <this value>,
 LexicalEnvironment = { ... },
 VariableEnvironment = { ... },
}

This 绑定:

​ 在全局执行上下文中,this 的值指向全局对象。(在浏览器中,this引用 Window 对象)。

​ 在函数执行上下文中,this 的值取决于该函数是如何被调用的。如果它被一个引用对象调用,那么 this 会被设置成那个对象,否则 this 的值被设置为全局对象或者undefined(在严格模式下)。例如:

let foo = {
 baz: function() {
 console.log(this);
 }
}
foo.baz();  // 'this' 引用 'foo', 因为 'baz' 被
       // 对象 'foo' 调用
let bar = foo.baz;
bar();    // 'this' 指向全局 window 对象,因为
       // 没有指定引用对象

词法环境

官方的 ES6 文档把词法环境定义为

​ 词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符和具体变量和函数的关联。一个词法环境由环境记录器和一个可能的引用外部词法环境的空值组成。


​ 简单来说词法环境是一种持有标识符—变量映射的结构。(这里的标识符指的是变量/函数的名字,而变量是对实际对象[包含函数类型对象]或原始数据的引用)。

​ 现在,在词法环境的内部有两个组件:(1) 环境记录器和 (2) 一个外部环境的引用。

  1. 环境记录器是存储变量和函数声明的实际位置。
  2. 外部环境的引用意味着它可以访问其父级词法环境(作用域)。

词法环境有两种类型:

  • 全局环境(在全局执行上下文中)是没有外部环境引用的词法环境。全局环境的外部环境引用是 null。它拥有内建的 Object/Array/等、在环境记录器内的原型函数(关联全局对象,比如 window 对象)还有任何用户定义的全局变量,并且 this的值指向全局对象。
  • 在函数环境中,函数内部用户定义的变量存储在环境记录器中。并且引用的外部环境可能是全局环境,或者任何包含此内部函数的外部函数。

环境记录器也有两种类型(如上!):

  1. 声明式环境记录器存储变量、函数和参数。
  2. 对象环境记录器用来定义出现在全局上下文中的变量和函数的关系。

简而言之

  • 在全局环境中,环境记录器是对象环境记录器。
  • 在函数环境中,环境记录器是声明式环境记录器。

注意

​对于函数环境,声明式环境记录器还包含了一个传递给函数的 arguments 对象(此对象存储索引和参数的映射)和传递给函数的参数的 length。

抽象地讲,词法环境在伪代码中看起来像这样:

GlobalExectionContext = {
 LexicalEnvironment: {
  EnvironmentRecord: {
   Type: "Object",
   // 在这里绑定标识符
  }
  outer: <null>
 }
}

FunctionExectionContext = {
 LexicalEnvironment: {
  EnvironmentRecord: {
   Type: "Declarative",
   // 在这里绑定标识符
  }
  outer: <Global or outer function environment reference>
 }
}

变量环境:

​它同样是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。

如上所述,变量环境也是一个词法环境,所以它有着上面定义的词法环境的所有属性。

​ 在 ES6 中,词法环境组件和变量环境的一个不同就是前者被用来存储函数声明和变量(letconst)绑定,而后者只用来存储 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 = {

 ThisBinding: <Global Object>,

 LexicalEnvironment: {
  EnvironmentRecord: {
   Type: "Object",
   // 在这里绑定标识符
   a: < uninitialized >,
   b: < uninitialized >,
   multiply: < func >
  }
  outer: <null>
 },

 VariableEnvironment: {
  EnvironmentRecord: {
   Type: "Object",
   // 在这里绑定标识符
   c: undefined,
  }
  outer: <null>
 }
}

FunctionExectionContext = {
 ThisBinding: <Global Object>,

 LexicalEnvironment: {
  EnvironmentRecord: {
   Type: "Declarative",
   // 在这里绑定标识符
   Arguments: {0: 20, 1: 30, length: 2},
  },
  outer: <GlobalLexicalEnvironment>
 },

VariableEnvironment: {
  EnvironmentRecord: {
   Type: "Declarative",
   // 在这里绑定标识符
   g: undefined
  },
  outer: <GlobalLexicalEnvironment>
 }
}

注意

​只有遇到调用函数 multiply 时,函数执行上下文才会被创建。

可能你已经注意到 letconst 定义的变量并没有关联任何值,但 var 定义的变量被设成了 undefined

​这是因为在创建阶段时,引擎检查代码找出变量和函数声明,虽然函数声明完全存储在环境中,但是变量最初设置为 undefinedvar 情况下),或者未初始化(letconst 情况下)。

​这就是为什么你可以在声明之前访问 var 定义的变量(虽然是 undefined),但是在声明之前访问 letconst 的变量会得到一个引用错误。

这就是我们说的变量声明提升。

执行阶段

​这是整篇文章中最简单的部分。在此阶段,完成对所有这些变量的分配,最后执行代码。

注意

​ 在执行阶段,如果 JavaScript 引擎不能在源码中声明的实际位置找到 let 变量的值,它会被赋值为 undefined

结论

​我们已经讨论过 JavaScript 程序内部是如何执行的。虽然要成为一名卓越的 JavaScript 开发者并不需要学会全部这些概念,但是如果对上面概念能有不错的理解将有助于你更轻松,更深入地理解其他概念,如变量声明提升,作用域和闭包。

参考文章:

https://juejin.cn/post/6844903682283143181

https://www.jianshu.com/p/6f8556b10379

https://juejin.cn/post/6844903704466833421

到此这篇关于JavaScript 中的执行上下文和执行栈实例讲解的文章就介绍到这了,更多相关JavaScript 中的执行上下文和执行栈内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
javascript import css实例代码
Jul 18 Javascript
cnblogs TagCloud基于jquery的实现代码
Jun 11 Javascript
jQuery的实现原理的模拟代码 -5 Ajax
Aug 07 Javascript
设为首页和收藏的Javascript代码(亲测兼容IE,Firefox,chrome等浏览器)
Nov 18 Javascript
JS实现清除指定cookies的方法
Sep 20 Javascript
JavaScript父子窗体间的调用方法
Mar 31 Javascript
学习JavaScript设计模式(单例模式)
Nov 26 Javascript
JS 通过系统时间限定动态添加 select option的实例代码
Jun 09 Javascript
JavaScript中最常见的三个面试题解析
Mar 04 Javascript
JavaScript定义及输出螺旋矩阵的方法详解
Dec 01 Javascript
深入理解JavaScript和TypeScript中的class
Apr 22 Javascript
小程序实现带年月选取效果的日历
Jun 27 Javascript
解决await在forEach中不起作用的问题
Feb 25 #Javascript
JS实现百度搜索框
Feb 25 #Javascript
基于JavaScript实现随机点名器
Feb 25 #Javascript
JavaScript仿京东轮播图效果
Feb 25 #Javascript
Vue基本指令实例图文讲解
Feb 25 #Vue.js
使用webpack和rollup打包组件库的方法
Feb 25 #Javascript
vue常用高阶函数及综合实例
Feb 25 #Vue.js
You might like
setTimeout和setInterval的浏览器兼容性分析
2007/02/27 Javascript
js获取height和width的方法说明
2013/01/06 Javascript
原生js做的手风琴效果的导航菜单
2013/11/08 Javascript
javascript生成随机颜色示例代码
2014/05/05 Javascript
JQuery中模拟image的ajaxPrefilter与ajaxTransport处理
2015/06/19 Javascript
jQuery实现下拉框选择图片功能实例
2015/08/08 Javascript
解析JavaScript面向对象概念中的Object类型与作用域
2016/05/10 Javascript
常用js,css文件统一加载方法(推荐) 并在加载之后调用回调函数
2016/09/23 Javascript
微信小程序 弹框和模态框实现代码
2017/03/10 Javascript
vue组件Prop传递数据的实现示例
2017/08/17 Javascript
vue.js实现只弹一次弹框
2018/01/29 Javascript
解决layui 复选框等内置控件不显示的问题
2018/08/14 Javascript
浅谈redux, koa, express 中间件实现对比解析
2019/05/23 Javascript
在vue和element-ui的table中实现分页复选功能
2019/12/04 Javascript
详解小程序BackgroundAudioManager踩坑之旅
2019/12/08 Javascript
详解小程序横屏方案对比
2020/06/28 Javascript
JS+CSS实现炫酷光感效果
2020/09/05 Javascript
Python2.x和3.x下maketrans与translate函数使用上的不同
2015/04/13 Python
Python使用正则表达式抓取网页图片的方法示例
2017/04/21 Python
python数据类型_元组、字典常用操作方法(介绍)
2017/05/30 Python
用 Python 爬了爬自己的微信朋友(实例讲解)
2017/08/25 Python
python通过getopt模块如何获取执行的命令参数详解
2017/12/29 Python
python实现整数的二进制循环移位
2019/03/08 Python
python字符串查找函数的用法详解
2019/07/08 Python
python django model联合主键的例子
2019/08/06 Python
Python 多进程原理及实现
2020/12/21 Python
用CSS3绘制三角形的简单方法
2015/07/17 HTML / CSS
CSS3中的clip-path使用攻略
2015/08/03 HTML / CSS
植物选择:Botanic Choice
2017/02/15 全球购物
Penhaligon’s英国官网:成立于1870年的英国香水制造商
2021/02/18 全球购物
大学班级计划书
2014/04/29 职场文书
建筑管理专业求职信
2014/07/28 职场文书
安全事故隐患排查治理制度
2015/08/05 职场文书
2016年员工年度考核评语
2015/12/02 职场文书
深度好文:50条没人告诉你的人生经验,句句精辟
2019/08/22 职场文书
我去timi了,一起去timi是什么意思?
2022/04/13 杂记