JavaScript执行上下文-执行栈

前言

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

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

什么是执行上下文

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

执行上下文的类型

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

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

ES3 执行上下文的内容

执行上下文是一个抽象的概念,咱们能够将它理解为一个 object ,一个执行上下文里包括如下内容:编程

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

变量对象(variable object 简称 VO

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

有一点须要注意,只有函数声明(function declaration)会被加入到变量对象中,而函数表达式(function expression)会被忽略。
复制代码
// 这种叫作函数声明,会被加入变量对象
function demo () {}
// tmp 是变量声明,也会被加入变量对象,可是做为一个函数表达式 demo2 不会被加入变量对象
var tmp = function demo2 () {}
复制代码

全局执行上下文和函数执行上下文中的变量对象还略有不一样,它们之间的差异简单来讲:浏览器

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

活动对象(activation object 简称 AO

函数进入执行阶段时,本来不能访问的变量对象被激活成为一个活动对象,自此,咱们能够访问到其中的各类属性。bash

其实变量对象和活动对象是一个东西,只不过处于不一样的状态和阶段而已。数据结构

做用域链(scope chain

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

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

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

关于 this 的建立细节,有点烦,有兴趣的话能够进入 这个章节 学习。

执行上下文数据结构模拟

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

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 --> 赋值(调用函数对象)
有没有发现这个建立执行上下文的阶段有变量和函数的初始化生命。这个操做就是 **变量声明提高**(变量和函数声明都会提高,可是函数提高更靠前)。
复制代码

执行阶段

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

销毁阶段

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

注意这只是通常状况,闭包的状况又有所不一样。

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

ES3执行上下文总结

对于 ES3 中的执行上下文,咱们能够用下面这个列表来归纳程序执行的整个过程:

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

ES5中的执行上下文

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

ExecutionContext = {
  ThisBinding = <this value>, LexicalEnvironment = { ... }, VariableEnvironment = { ... }, } 复制代码

This Binding

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

词法环境(Lexical Environment)

词法环境有两个组成部分

  • 一、环境记录:存储变量和函数声明的实际位置
  • 二、对外部环境的引用:能够访问其外部词法环境

词法环境有两种类型

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

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

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>  
  }  
}
复制代码

变量提高的缘由:在建立阶段,函数声明存储在环境中,而变量会被设置为 undefined(在 var 的状况下)或保持未初始化(在 letconst 的状况下)。因此这就是为何能够在声明以前访问 var 定义的变量(尽管是 undefined ),但若是在声明以前访问 letconst 定义的变量就会提示引用错误的缘由。这就是所谓的变量提高。

ES5 执行上下文总结

对于 ES5 中的执行上下文,咱们能够用下面这个列表来归纳程序执行的整个过程:

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

执行栈

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

当 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 引擎建立了一个全局执行上下文并把它压入当前执行栈。当遇到 first() 函数调用时,JavaScript 引擎为该函数建立一个新的执行上下文并把它压入当前执行栈的顶部。

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

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

结论

  1. 执行上下文建立阶段分为绑定this,建立词法环境,变量环境三步,二者区别在于词法环境存放函数声明与const let声明的变量,而变量环境只存储var声明的变量。
  2. 词法环境主要由环境记录与外部环境引入记录两个部分组成,全局上下文与函数上下文的外部环境引入记录不同,全局为null,函数为全局环境或者其它函数环境。环境记录也不同,全局叫对象环境记录,函数叫声明性环境记录。
  3. 你应该明白为何会存在变量提高,函数提高,而let const没有。
  4. ES3以前的变量对象与活动对象的概念在ES5以后由词法环境,变量环境来解释,二者概念不冲突,后者理解更为通俗易懂。不得不说相关文章也是看的我心累,也但愿对有缘的你有所帮助,那么到这里,本文结束。

参考

JavaScript执行上下文和执行栈

JavaScript深刻之执行上下文栈

相关文章
相关标签/搜索