执行上下文和作用域链

文章主要参考了文末列出的一些相关文章,加上了一部分自己的理解和对其他文章的整理,有不足之处敬请指出

1 执行上下文

1.1 什么是执行上下文

执行上下文(Execution context 简称 EC)就是 js 的执行环境,它包括 this 的值、变量、对象和函数。它是一个抽象的概念。

变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。

例如,当执行到可执行代码片段(通常是函数调用)的时候,JS 引擎会做一些“准备工作”,而这个“准备工作”,就叫做 执行上下文

1.2 执行上下文的类型

Javascript 中有三种执行上下文类型:

  1. 全局执行上下文
    • Global execution context (GEC)
    • 是默认的执行上下文,文件第一次加载到浏览器中,js 代码在默认执行上下文中开始执行
    • 一个程序中只会存在一个全局上下文
    • 全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器
    • 全局上下文是 最外层的上下文,根据 ECMAScript 实现的宿主环境,表示全局上下文的对象可能不一样:1. 浏览器中,全局上下文是 window 对象,因此通过 var 定义的全局变量和函数都会成为 window 对象的属性和方法;2. node 环境中,全局上下文是 global
  2. 函数执行上下文
    • Functional execution context (FEC)
    • 函数调用时,会创建一个函数执行上下文
    • 函数被重复调用,每次调用都会创建一个新的执行上下文
  3. Eval 函数执行上下文 ———— eval 函数内的执行上下文

2 执行上下文栈

执行上下文栈(Execution context stack 简称 ECS)是执行 js 代码时创建的执行栈结构,遵循 LIFO(后进先出)特性,用于管理在代码执行期间创建的执行上下文。

  • GEC(全局执行上下文)在栈最底层
  • 当 JS 引擎发现一处函数调用,会创建一个 FEC(函数执行上下文),并 push 进栈,将控制权交给该上下文
  • 在函数执行完之后,上下文栈弹出(pop)该 FEC(函数执行上下文),并将控制权返还给之前的执行上下文

例如:下面这段代码

var a = 10;

function functionA() {
    console.log("Start function A");
    
    function functionB(){
        console.log("In function B");
    }
    
    functionB();
}

functionA();
console.log("GlobalContext");
复制代码
  1. 代码执行之前,JS 引擎将全局执行上下文推入执行上下文栈
  2. funcA 在全局上下文中被调用,funcA 的执行上下文入栈,并运行该函数
  3. 在 funcA 的上下文中,调用了 funcB 函数,funcB 的执行上下文入栈,并运行该函数
  4. funcB 函数中所有代码执行完毕,funcB 函数的上下文出栈,继续执行 funcA 函数
  5. funcA 函数中所有代码执行完毕,funcA 函数的上下文出栈,继续执行全局上下文中的代码
  6. 所有代码执行完毕,则全局上下文弹出

入栈和出栈示意图如下:

image.png

image.png

3 创建执行上下文

以上介绍了 JS 引擎如何处理执行上下文(push 和 pop),下面介绍 JS 引擎如何创建执行上下文。

这个过程分为两个阶段:

  • 创建阶段
  • 执行阶段

3.1 创建阶段

函数执行上下文的创建阶段,发生在函数调用时,但还未开始执行函数体内的具体代码之前。
在这个阶段,JS 对整个函数进行了一个编译,主要做了以下三件事:

  1. 创建变量对象
    • variable object 简称 VO
    • 根据当前函数的 参数列表(arguments)初始化一个 arguments 对象
    • 根据函数声明生成对应的属性,其值为一个指向内存中函数的引用指针,如果函数名已存在,则覆盖
    • 根据变量声明生成对应的属性,由于变量声明提升,此时初始值为 undefined,需要等到执行阶段才会有确定的值。如果变量名已声明,则忽略该变量声明
    • 虽然无法通过代码访问变量对象,但是后台处理数据会用到它
  2. 创建作用域链
    • 变量对象创建完,JS 引擎就开始初始化作用域链
  3. 确定 this 的值
    • 全局执行上下文中,this 的值指向全局对象
    • 函数执行上下文中,this 的值取决于函数的调用方式

注意:只有 函数声明 会被加入到变量对象中,函数表达式 会被忽略

// 函数声明,会被加入变量对象
function a () {}

// b 是变量声明,也会被加入变量对象,但是作为一个函数表达式 _b 不会被加入变量对象
var b = function _b () {}
复制代码

3.1.1 示例

下面通过一个例子来说明

function funA (a, b) {
  var c = 3;
  var d = 2;
  
  d = function() {
    return a - b;
  }
}

funA(3, 2);
复制代码

当调用 funcA 和执行 funcA 前的这段时间(也就是创建阶段),JS 引擎为 funcA 创建了一个变量对象如下:

executionContextObj = {
   variableObject: {}, // funA 中所有的变量,参数和内部函数
   scopeChain: [], // 当前函数涉及的作用域的列表
   this 
}
复制代码

变量对象包含的内容如下:

variableObject = {
  argumentObject : {
    0: a,
    1: b,
    length: 2
  },
  a: 3,
  b: 2
  c: undefined,
  d: undefined // 执行到第 6 行时,改为 pointer to the function defintion of d
}
复制代码
  1. argumentObject 代表入参,funcA 有两个入参,因此 length 属性值为 2。0 和 1 分别对应着两个入参 a 和 b
  2. a 和 b 两个入参的值已经确定,因此 a 初始化为 3,b 初始化为 2
  3. 函数中的变量 c 和 d 会被初始化为 undefined
  4. 执行到 d = function() {…} 时,d 的值会被修改为指向堆内存中的一段函数定义

3.2 执行阶段

  • 在这个阶段,原本不能访问的变量对象被激活成一个活动对象activation object 简称 AO),自此,我们可以访问到其中的各种属性
  • JS 引擎开始对定义的变量赋值,开始顺着作用域链访问变量
  • 如果内部有函数调用,就创建一个新的执行上下文压入执行栈,并把控制权交出

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

上一段示例中,在执行阶段完成后,变量对象的内容改为:

variableObject = {
  argumentObject : {
    0: a,
    1: b,
    length: 2
  },
  a: 3,
  b: 2,
  c: 3,
  d: undefined then pointer to the function defintion of d
}
复制代码

4 完整示例

完整示例链接查看:

Execution context, Scope chain and JavaScript internals 中 Complete example 一节。

代码执行过程中的变量对象变化如下:

a = 1;
var b = 2;

cFunc = function(e) {
  var c = 10;
  var d = 15;
  a = 3
  function dFunc() {
    var f = 5;
  }
  
  dFunc();
}

cFunc(10);
复制代码

全局编译阶段:

globalExecutionContextObj = {
  activationbj: {
      argumentObj : {
          length:0
      },
      b: undefined,
      cFunc: Pointer to the function definition
  },
  scopeChain: [GLobal execution context variable object],
  this: value of this
}
复制代码

全局执行阶段:

globalExecutionContextObj = {
  activationbj: {
      argumentObj : {
          length:0
      },
      b: 2,
      cFunc: Pointer to the function definition,
      a: 1
  },
  scopeChain: [GLobal execution context variable object],
  this: value of this
}
复制代码

cFunc的编译阶段:

cFuncExecutionContextObj = {
  activationbj: {
      argumentObj : {
          0: e,
          length:1
      },
      e: 10,
      c: undefined,
      d: undefined
      dFunc: Pointer to the function definition,
  },
  scopeChain: [cFunc variable object, Global exection context variable object],
  this: value of this
}
复制代码

cFunc的执行阶段:

cFuncExecutionContextObj = {
  activationbj: {
      argumentObj : {
          0: e,
          length:1
      },
      e: 10,
      c: 10,
      d: 15
      dFunc: Pointer to the function definition,
  },
  scopeChain: [cFunc variable object, Global exection context variable object],
  this: value of this
}
复制代码

5 作用域链

作用域链是当前函数所在的 “变量对象” 的链表。

  • 当查找变量的时候,会先从当前上下文的变量对象中查找
  • 如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找
  • 一直找到全局上下文的变量对象

ES5 中对于执行上下文的修改

参考文章

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享