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面向对象之四 继承
Feb 08 Javascript
js将控件隐藏及display属性的使用介绍
Dec 30 Javascript
ajax请求乱码的解决方法(中文乱码)
Apr 10 Javascript
浅谈JSON.parse()和JSON.stringify()
Jul 14 Javascript
自动化测试读写64位操作系统的注册表
Aug 15 Javascript
利用Vue.js指令实现全选功能
Sep 08 Javascript
Vue学习笔记之表单输入控件绑定
Sep 05 Javascript
vue2.0学习之axios的封装与vuex介绍
May 28 Javascript
JS在if中的强制类型转换方式
Jul 15 Javascript
Vue 表情包输入组件的实现代码
Jan 21 Javascript
node.js中process进程的概念和child_process子进程模块的使用方法示例
Feb 11 Javascript
vue组件库的在线主题编辑器的实现思路
Apr 03 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
ThinkPHP CURD方法之field方法详解
2014/06/18 PHP
PHP MVC框架skymvc支持多文件上传
2016/05/26 PHP
php实现的生成迷宫与迷宫寻址算法完整实例
2017/11/06 PHP
jquery控制listbox中项的移动并排序的实现代码
2010/09/28 Javascript
Js-$.extend扩展方法使方法参数更灵活
2013/01/15 Javascript
判断文档离浏览器顶部的距离的方法
2014/01/08 Javascript
JavaScript每天定时更换皮肤样式的方法
2015/07/01 Javascript
详解JavaScript ES6中的Generator
2015/07/28 Javascript
一篇看懂vuejs的状态管理神器 vuex状态管理模式
2017/04/20 Javascript
基于jQuery封装的分页组件
2017/06/26 jQuery
解决Vue2.0自带浏览器里无法打开的原因(兼容处理)
2017/07/28 Javascript
微信小程序开发之点击按钮退出小程序的实现方法
2019/04/26 Javascript
Javascript操作select控件代码实例
2020/02/14 Javascript
vue实现标签云效果的示例
2020/11/09 Javascript
[55:23]VGJ.T vs Winstrike 2018国际邀请赛小组赛BO2 第二场 8.17
2018/08/20 DOTA
Python3 jupyter notebook 服务器搭建过程
2018/11/30 Python
selenium+python自动化测试之鼠标和键盘事件
2019/01/23 Python
Pandas中resample方法详解
2019/07/02 Python
python manage.py runserver流程解析
2019/11/08 Python
使用Python进行防病毒免杀解析
2019/12/13 Python
python实现最短路径的实例方法
2020/07/19 Python
Python制作数据预测集成工具(值得收藏)
2020/08/21 Python
Numpy中np.max的用法及np.maximum区别
2020/11/27 Python
Weekendesk意大利:探索多种引人入胜的周末主题
2016/10/14 全球购物
美国女孩服装购物网站:Justice
2017/03/04 全球购物
天巡全球:Skyscanner Global
2017/06/20 全球购物
亿阳信通股份有限公司笔试题(C#)
2016/03/04 面试题
规划编制实施方案
2014/03/15 职场文书
骨干教师考核方案
2014/05/09 职场文书
给女朋友道歉的话大全
2015/01/20 职场文书
介绍信格式
2015/01/30 职场文书
绍兴鲁迅故居导游词
2015/02/09 职场文书
2016年学校禁毒宣传活动工作总结
2016/04/05 职场文书
go 原生http web 服务跨域restful api的写法介绍
2021/04/27 Golang
教你使用RustDesk 搭建一个自己的远程桌面中继服务器
2022/08/14 Servers
mysql通过group by分组取最大时间对应数据的两种有效方法
2022/09/23 MySQL