首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >「查缺补漏」JavaScript执行上下文-执行栈

「查缺补漏」JavaScript执行上下文-执行栈

作者头像
前端老王
发布于 2020-09-28 02:45:02
发布于 2020-09-28 02:45:02
62300
代码可运行
举报
文章被收录于专栏:前端时空前端时空
运行总次数:0
代码可运行

前言

突然觉得对于一名JavaScript开发者而言,需要知道JavaScript程序内部是如何运行的,那么对于此章节执行上下文和执行栈的理解很重要,对理解其他JavaScript概念(变量声明提示,作用域和闭包)都有帮助。

看了很多相关文章,写得很好,总结了ES3以及ES6对于执行上下文概念的描述,以及新的概念介绍。

什么是执行上下文

简而言之,执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行。

执行上下文的类型

JavaScript 中有三种执行上下文类型

  • 「全局执行上下文」 — 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
  • 「函数执行上下文」 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。
  • 「Eval 函数执行上下文」 — 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用 eval,所以在这里我不会讨论它。

ES3 执行上下文的内容

执行上下文是一个抽象的概念,我们可以将它理解为一个 object ,一个执行上下文里包括以下内容:

  1. 变量对象 VO
  2. 活动对象 AO
  3. 作用域链
  4. 调用者信息 this

变量对象(variable object 简称 VO

每个执行环境文都有一个表示变量的对象——「变量对象」,全局执行环境的变量对象始终存在,而函数这样局部环境的变量,只会在函数执行的过程中存在,在函数被调用时且在具体的函数代码运行之前,JS 引擎会用当前函数的「参数列表」arguments)初始化一个 “变量对象” 并将当前执行上下文与之关联 ,函数代码块中声明的 「变量」「函数」 将作为属性添加到这个变量对象上。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
有一点需要注意,只有函数声明(function declaration)会被加入到变量对象中,而函数表达式(function expression)会被忽略。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 这种叫做函数声明,会被加入变量对象
function demo () {}
// tmp 是变量声明,也会被加入变量对象,但是作为一个函数表达式 demo2 不会被加入变量对象
var tmp = function demo2 () {}

全局执行上下文和函数执行上下文中的变量对象还略有不同,它们之间的差别简单来说:

  1. 「全局上下文中的变量对象就是全局对象」,以浏览器环境来说,就是 window 对象。
  2. 「函数执行上下文中的变量对象内部定义的属性」,是不能被直接访问的,只有当函数被调用时,变量对象(VO)被激活为活动对象(AO)时,我们才能访问到其中的属性和方法。

活动对象(activation object 简称 AO

函数进入执行阶段时,原本不能访问的变量对象被激活成为一个活动对象,自此,我们可以访问到其中的各种属性。

「其实变量对象和活动对象是一个东西,只不过处于不同的状态和阶段而已。」

作用域链(scope chain

「作用域」 规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做 「作用域链」

当前可执行代码块的调用者(this)

如果当前函数被作为对象方法调用或使用 bind call applyAPI 进行委托调用,则将当前代码块的调用者信息(this value)存入当前执行上下文,否则默认为全局对象调用。

关于 this 的创建细节,有点烦,有兴趣的话可以进入 这个章节 学习。

执行上下文数据结构模拟

如果将上述一个完整的执行上下文使用代码形式表现出来的话,应该类似于下面这种:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
executionContext:{
    [variable object | activation object]{
        arguments,
        variables: [...],
        funcions: [...]
    },
    scope chain: variable object + all parents scopes
    thisValue: context object
}

ES3中的执行上下文生命周期

执行上下文的生命周期有三个阶段,分别是:

  • 创建阶段
  • 执行阶段
  • 销毁阶段
创建阶段

函数执行上下文的创建阶段,发生在函数调用时且在执行函数体内的具体代码之前,在创建阶段,JS 引擎会做如下操作:

全局执行上下文
  • 执行全局代码前,创建一个全局执行上下文
  • 对全局数据进行预处理
    • 这一阶段会进行「变量和函数的初始化声明」
    • var 定义的全局变量--> undefined 添加为window属性
    • function 声明的全局函数 –-> 赋值(fun) 添加为window属性
    • this --> 赋值(window)
函数执行上下文
  • 在调用函数时,准备执行函数体之前,创建对应的函数执行上下文对象
  • 对局部数据进行预处理
    • 形参变量==》赋值(实参)--》添加为执行上下文的属性
    • arguments-->赋值-->(实参列表),添加为执行上下文属性
    • var 定义的局部变量 –-> undefined 添加为执行上下文属性
    • function 神明的函数 --> 赋值(fun) 添加为执行上下文属性
    • 构建作用域链(前面已经说过构建细节)
    • this --> 赋值(调用函数对象)
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
有没有发现这个创建执行上下文的阶段有变量和函数的初始化生命。这个操作就是 **变量声明提升**(变量和函数声明都会提升,但是函数提升更靠前)。
执行阶段

执行阶段中,JS 代码开始逐条执行,在这个阶段,JS 引擎开始对定义的变量赋值、开始顺着作用域链访问变量、如果内部有函数调用就创建一个新的执行上下文压入执行栈并把控制权交出……

销毁阶段

一般来讲当函数执行完成后,当前执行上下文(局部环境)会被弹出执行上下文栈并且销毁,控制权被重新交给执行栈上一层的执行上下文。

❝注意这只是一般情况,闭包的情况又有所不同。 ❞

闭包的定义:「有权访问另一个函数内部变量的函数」。简单说来,如果一个函数被作为另一个函数的返回值,并在外部被引用,那么这个函数就被称为闭包。

ES3执行上下文总结

对于 ES3 中的执行上下文,我们可以用下面这个列表来概括程序执行的整个过程:

  1. 函数被调用
  2. 在执行具体的函数代码之前,创建了执行上下文
  3. 进入执行上下文的创建阶段:
    1. 对于每个找到的变量声明,用它们的原生变量名,在变量对象中创建一个属性,并且使用 undefined 来初始化
    2. 如果变量名作为属性在变量对象中已存在,则不做任何处理并接着扫描
    3. 对于每个找到的函数,用它们的原生函数名,在变量对象中创建一个属性,该属性里存放的是一个指向实际内存地址的指针
    4. 如果函数名称已经存在了,属性的引用指针将会被覆盖
    5. 初始化作用域链
    6. 创建 arguments object 检查上下文中的参数,初始化名称和值并创建引用副本
    7. 扫描上下文找到所有函数声明:
    8. 扫描上下文找到所有var的变量声明:
    9. 确定 this
  4. 进入执行上下文的执行阶段:
    1. 在上下文中运行/解释函数代码,并在代码逐行执行时分配变量值。

ES5中的执行上下文

ES5 规范又对 ES3 中执行上下文的部分概念做了调整,最主要的调整,就是去除了 ES3 中变量对象和活动对象,以 「词法环境组件(」 「LexicalEnvironment component)」「变量环境组件(」 「VariableEnvironment component)」 替代。所以 ES5 的执行上下文概念上表示大概如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ExecutionContext = {
  ThisBinding = <this value>,
  LexicalEnvironment = { ... },
  VariableEnvironment = { ... },
}

This Binding

  • 「全局」执行上下文中,this 的值指向全局对象,在浏览器中this 的值指向 window对象,而在nodejs中指向这个文件的module对象。
  • 「函数」执行上下文中,this 的值取决于函数的调用方式。具体有:默认绑定、隐式绑定、显式绑定(硬绑定)、new绑定、箭头函数,具体内容会在【this全面解析】部分详解。

词法环境(Lexical Environment)

词法环境有两个「组成部分」

  • 1、「环境记录」:存储变量和函数声明的实际位置
  • 2、「对外部环境的引用」:可以访问其外部词法环境

词法环境有两种「类型」

  • 1、「全局环境」:是一个没有外部环境的词法环境,其外部环境引用为 「null」。拥有一个全局对象(window 对象)及其关联的方法和属性(例如数组方法)以及任何用户自定义的全局变量,this 的值指向这个全局对象。
  • 2、「函数环境」:用户在函数中定义的变量被存储在「环境记录」中,包含了arguments 对象。对外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境。

直接看伪代码可能更加直观

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
GlobalExectionContext = {  // 全局执行上下文
  LexicalEnvironment: {    	  // 词法环境
    EnvironmentRecord: {   		// 环境记录
      Type: "Object",      		   // 全局环境
      // 标识符绑定在这里 
      outer: <null>  	   		   // 对外部环境的引用
  }  
}

FunctionExectionContext = { // 函数执行上下文
  LexicalEnvironment: {  	  // 词法环境
    EnvironmentRecord: {  		// 环境记录
      Type: "Declarative",  	   // 函数环境
      // 标识符绑定在这里 			  // 对外部环境的引用
      outer: <Global or outer function environment reference>  
  }  
}

变量环境

变量环境也是一个词法环境,因此它具有上面定义的词法环境的所有属性。

在 ES6 中,「词法」 环境和 「变量」 环境的区别在于前者用于存储**函数声明和变量( letconst「绑定,而后者仅用于存储」变量( var )**绑定。

使用例子进行介绍

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
let a = 20;  
const b = 30;  
var c;

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

c = multiply(20, 30);

执行上下文如下所示

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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>  
  }  
}

「变量提升」的原因:在创建阶段,函数声明存储在环境中,而变量会被设置为 undefined(在 var 的情况下)或保持未初始化(在 letconst 的情况下)。所以这就是为什么可以在声明之前访问 var 定义的变量(尽管是 undefined ),但如果在声明之前访问 letconst 定义的变量就会提示引用错误的原因。这就是所谓的变量提升。

ES5 执行上下文总结

对于 ES5 中的执行上下文,我们可以用下面这个列表来概括程序执行的整个过程:

  1. 程序启动,全局上下文被创建
    1. 创建 「对象环境记录器」,它持有 「变量声明语句」 在执行上下文中创建的绑定关系(负责处理 var 定义的变量,初始值为 undefined 造成声明提升)
    2. 创建 「外部环境引用」,值为 「null」
    3. 创建 「对象环境记录器」 ,它用来定义出现在 「全局上下文」 中的变量和函数的关系(负责处理 letconst 定义的变量)
    4. 创建 「外部环境引用」,值为 「null」
    5. 创建全局上下文的「词法环境」
    6. 创建全局上下文的「变量环境」
    7. 确定 this 值为全局对象(以浏览器为例,就是 window
  2. 函数被调用,函数上下文被创建
    1. 创建 「声明式环境记录器」 ,存储变量、函数和参数,它包含了一个传递给函数的 「arguments」 对象(此对象存储索引和参数的映射)和传递给函数的参数的 「length」。(负责处理 var 定义的变量,初始值为 undefined 造成声明提升)
    2. 创建 「外部环境引用」,值为全局对象,或者为父级词法环境(作用域)
    3. 创建 「声明式环境记录器」 ,存储变量、函数和参数,它包含了一个传递给函数的 「arguments」 对象(此对象存储索引和参数的映射)和传递给函数的参数的 「length」。(负责处理 letconst 定义的变量)
    4. 创建 「外部环境引用」,值为全局对象,或者为父级词法环境(作用域)
    5. 创建函数上下文的「词法环境」
    6. 创建函数上下文的「变量环境」
    7. 确定 this
  3. 进入函数执行上下文的执行阶段:
    1. 在上下文中运行/解释函数代码,并在代码逐行执行时分配变量值。

执行栈

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

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

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

让我们通过下面的代码示例来理解:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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');

js执行栈

上述代码的执行上下文栈。

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

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

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

结论

  1. 执行上下文创建阶段分为绑定this,创建词法环境,变量环境三步,两者区别在于词法环境存放函数声明与const let声明的变量,而变量环境只存储var声明的变量。
  2. 词法环境主要由环境记录与外部环境引入记录两个部分组成,全局上下文与函数上下文的外部环境引入记录不一样,全局为null,函数为全局环境或者其它函数环境。环境记录也不一样,全局叫对象环境记录,函数叫声明性环境记录。
  3. 你应该明白为什么会存在变量提升,函数提升,而let const没有。
  4. ES3之前的变量对象与活动对象的概念在ES5之后由词法环境,变量环境来解释,两者概念不冲突,后者理解更为通俗易懂。不得不说相关文章也是看的我心累,也希望对有缘的你有所帮助,那么到这里,本文结束。
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-09-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端时空 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
(ES5版)深入理解 JavaScript 执行上下文和执行栈
最近在研究 JavaScript 基础性的东西,但是看到对于执行上下文的解释我发现有两种,一种是执行上下文包含:scope(作用域)、variable object(变量对象)、this value(this 值),另外一个种是包含:lexical environment(词法环境)、variable environment(变量环境)、this value(this 值)。
桃翁
2020/03/25
9870
(ES5版)深入理解 JavaScript 执行上下文和执行栈
理解JavaScript 中的执行上下文和执行栈
变量提升的原因:在创建阶段,函数声明存储在环境中,而变量会被设置为undefined(在 var 的情况下)或保持未初始化(在 let 和 const 的情况下)。所以这就是为什么可以在声明之前访问 var 定义的变量(尽管是 undefined),但如果在声明之前访问 let 和 const 定义的变量就会提示引用错误的原因。这就是所谓的变量提升。 执行阶段 此阶段,完成对所有变量的分配,最后执行代码。 如果 Javascript 引擎在源代码中声明的实际位置找不到 let 变量的值,那么将为其分配 undefined 值。
用户7657330
2020/08/16
7070
深入理解执行上下文和执行栈
执行上下文、执行栈、作用域链、闭包,这其实是一整套相关的东西,之前转载的文章也有讲到这些。下面两篇文章会更加详细地解释这些概念。
Chor
2019/11/07
8280
JS词法环境和执行上下文
JavaScript是一门解释性动态语言,但同时它也是一门充满神秘感的语言。如果要成为一名优秀的JS开发者,那么对JavaScript程序的内部执行原理要有所了解。
hellocoder2029
2022/10/21
1.4K0
如何理解js的执行上下文与执行栈
执行上下文和执行栈是js执行机制中的两个概念,要想深入的对js进行理解与应用,理解js的机制很重要,下面来说一下什么是执行上下文,什么又是执行栈。
OECOM
2020/07/02
1.9K0
如何理解js的执行上下文与执行栈
从 ECMAScript 6 角度谈谈执行上下文
起因是最近了解JS执行上下文的时候,发现很多书籍和资料,包括《JavaScript高级程序设计》、《JavaScript权威指南》和网上的一些博客专栏,都是从 ES3 角度来谈执行上下文,用ES6规范解读的比较少,所以想从ES6的角度看一下执行上下文。
归思君
2023/12/22
2010
从 ECMAScript 6 角度谈谈执行上下文
前端冲刺必备指南-执行上下文/作用域链/闭包/一等公民
前言 大家好,我是吒儿?,每天努力一点点?,就能升职加薪?当上总经理出任CEO迎娶白富美走上人生巅峰?,想想还有点小激动呢?。 这是我的第11期文章内容✍,我并不希望把?这篇文章内容成为笔记去记,或者
达达前端
2020/05/20
8740
万字干货!详解JavaScript执行过程
栈的特点是"LIFO,即后进先出(Last in, first out)"。数据存储时只能从顶部逐个存入,取出时也需从顶部逐个取出。
童欧巴
2021/08/20
1.1K0
万字干货!详解JavaScript执行过程
JavaScript复习之作用域链
之前复习到的执行上下文里在ES3版本里说过它包含的三大内容是变量对象,作用域链,this指向。以及ES6版本里词法环境中有的外部环境引用outer。 都是涉及到了今天要复习的知识点, 也就是JavaScript中存在着基于作用域而产生的一条类似链表的链路。
用户6256742
2024/06/28
1020
JavaScript复习之作用域链
JS入门难点解析8-作用域,作用域链,执行上下文,执行上下文栈等分析
(注1:如果有问题欢迎留言探讨,一起学习!转载请注明出处,喜欢可以点个赞哦!) (注2:更多内容请查看我的目录。)
love丁酥酥
2018/08/27
7220
解读闭包,这次从ECMAScript词法环境,执行上下文说起
对于x年经验的前端仔来说,项目也做了好些个了,各个场景也接触过一些。但是假设真的要跟面试官敞开来撕原理,还是有点慌的。看到很多大神都在手撕各种框架原理还是有点羡慕他们的技术实力,羡慕不如行动,先踏踏实实啃基础。嗯...今天来聊聊闭包!
程序员白彬
2020/08/10
1.3K0
解读闭包,这次从ECMAScript词法环境,执行上下文说起
深入理解JavaScript 执行上下文
只有理解了执行上下文,才能更好地理解 JavaScript 语言本身,比如变量提升,作用域,闭包等
木子星兮
2020/07/16
4030
【译】JS的执行上下文和环境栈是什么?
这篇文章中,我将深入探讨JavaScript中的一个最基本的部分,即执行上下文(或称环境)。读过本文后,你将更加清楚地了解到解释器尝试做什么,为什么在声明某些函数/变量之前,可以使用它们以及它们的值是如何确定的。
Jimmy_is_jimmy
2019/07/31
8510
兄台: 作用域、执行上下文了解一下
变量的词法作用域(简称:作用域)是程序中可以访问变量的区域。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。
前端柒八九
2022/08/25
5560
兄台: 作用域、执行上下文了解一下
JavaScript-执行上下文
(1)在JavaScript解释器内部,每次调用执行上下文,分为两个阶段,①创建阶段 和 ②激活/代码执行阶段。
WEBING
2019/03/13
4350
JavaScript-执行上下文
高能预警:执行上下文及执行栈
打个比方:比如你尿急,是不是得上厕所。尿急是一个动作,就相当于我们的程序代码,那么厕所是不是一个环境,你总不能随地大小便吧。所以这个环境就是指我们程序执行的环境,从抽象层面上讲就是你的给程序代码一个可以执行环境。
公众号---人生代码
2021/01/27
3300
高能预警:执行上下文及执行栈
JS执行上下文与调用栈
本文分享 了 JavaScript 基础的两个方面:执行上下文和调用堆栈。每当 JavaScript 代码运行时,它都在执行上下文中运行;调用栈则可以在脚本调用多个函数时,跟踪每个函数在完成执行时应该返回的控制点。
前端开发博客
2020/11/04
1.7K0
JS到底是怎么执行的:一文彻底搞清执行上下文
所有JavaScript代码都需要在某种环境中托管和运行。在大多数情况下,这种环境是一个web浏览器。
前端修罗场
2022/07/29
1.6K0
JS到底是怎么执行的:一文彻底搞清执行上下文
Javascript中你必须理解的执行上下文和调用栈
版权声明:署名,允许他人基于本文进行创作,且必须基于与原先许可协议相同的许可协议分发本文 (Creative Commons)
六小登登
2019/08/14
5230
JavaScript执行上下文
JavaScript中的执行上下文是一个抽象的概念,用于描述代码在运行时的环境和状态。执行上下文包含了变量、函数声明、作用域链等信息,它的创建和销毁过程是JavaScript代码执行的基础。
堕落飞鸟
2023/05/17
3370
推荐阅读
相关推荐
(ES5版)深入理解 JavaScript 执行上下文和执行栈
更多 >
交个朋友
加入腾讯云官网粉丝站
蹲全网底价单品 享第一手活动信息
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档