执行上下文Context

193 阅读6分钟

原文链接:执行上下文(context)

1. 执行上下文定义(以下简称“上下文”):

当JavaScript代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context);简而言之,上下文是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在上下文中运行

  1. 变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象(variable object),而这个上下文中定义的所有变量和函数都存在于这个对象上。

  2. 上下文的三个重要属性:

    • 变量对象(Variable object,VO)

    • 作用域链(Scope chain)

    • this

2. 上下文的类型

  • 全局上下文 —— 最外层的上下文。根据 ECMAScript 实现的宿主环境,表示全局上下文的对象可能不一样。在浏览器中,全局上下文就是我们常说的 window 对象。
  • 函数上下文 —— 每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。(同一个函数被多次调用,都会创建一个新的上下文)上下文是函数,则其活动对象(activation object)用作变量对象。活动对象最初只有一个定义变量:arguments 。
  • Eval 函数上下文 — 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用 eval,所以不会讨论它。

3. 上下文的生命周期

上下文的生命周期分两个阶段:创建阶段和执行阶段

创建阶段会发生两件事:

  1. 创建词法环境(LexicalEnvironment)

  2. 创建变量环境(VariableEnvironment)

    用伪代码表示:

    ExecutionContext = {
      LexicalEnvironment: {},
      VariableEnvironment: {},
    }
    

词法环境(LexicalEnvironment)定义

简而言之是标识符-变量的映射(identifier-variable mapping标识符指的是函数或变量的名称,变量指得是真正的引用对象或基本类型数据。它包含三个结构:

  1. 词法环境内部的环境记录(Environment Record);

  2. 一个指向外层词法环境的可空引用(Outer environment);

  3. this(this的绑定)

环境记录(Environment Record)

​ 分两种:

​ 1. 声明式环境记录(Declarative environment record)—— 用在函数上下文中,包含变量、函数和arguments 对象

​ 2. 对象式环境记录(Object environment record)—— 用于全局上下文中,主要用于记录全局的对象,函数和变量

外部环境(Outer environment)

​ outer 指向外部的词法环境,这样如果在当前的词法环境里面没有找到变量,可以通过outer获取到外部的 数据。全局上下文的outer总是指向null。

this

  • 在全局执行上下文中,this 指向全局对象 window(在浏览器中)
  • 在函数执行上下文中,this 取决于函数是如何被调用的。()

变量环境(Variable Environment)定义

变量环境也是一个词法环境,它和词法环境是一样的。区别在于,在 ES6 中,词法环境用来存储函数声明和 letconst 声明的变量,变量环境仅仅用来存储 var 声明的变量。

例子:

let a = 1;
const b = 2;
var c = 3;

function test(a, b) {
  let d = 3;
  return a + b;
}

c = test(a, b);

上述代码全局上下文创建情况

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object", // 对象式环境记录用于全局上下文中
      a: <uninitialized>, // let声明在创建时期uninitialized(即未初始化)
      b: <uninitialized>,
      test: <function>
    }
    outer: <null>,
    this: <global object>
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      c: undefined, // var 声明undefined
    }
    outer: <null>,
    this: <global object>
  }
}

test 函数执行时,函数上下文的创建情况:

TestFunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative", // 函数上下文的环境记录为Declarative类型
      d: <uninitialized>,
      Arguments: {0: 1, 1: 2, length: 2}// 函数上下文包含Arguments对象
    }
    outer: <GlobalExectionContext>, // 指向上层上下文
    this: <global object>
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
    }
    outer: <GlobalExectionContext>,
    this: <global object>
  }
}

letconst 声明的变量在创建阶段(creation phase) 是未初始化的,但是 var 声明的变量被赋予了 undefined, 前者将会形成暂时性死区,因此提前使用它们将会报错。

这其实就是 var 变量提升 和 let 的TDZ(临时性死区),并且函数的变量提升优先级高,因此会出现变量覆盖,另外函数表达式并不会提升

注意,在执行阶段,如果引擎发现 let 声明的变量并没有被赋值,引擎将会把它赋值为 undefined

执行阶段

完成对所有这些变量的分配,最后执行代码。(上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数)

上述例子执行阶段全局上下文情况:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object", // 对象式环境记录用于全局上下文中
      a: 1, // 执行完a = 1
      b: 2, // 执行完b = 2
      test: <function>
    }
    outer: <null>,
    this: <global object>
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      c: 3, // 执行完c = 3
    }
    outer: <null>,
    this: <global object>
  }
}

执行阶段,test函数上下文情况:

TestFunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative", // 函数上下文的环境记录为Declarative类型
      d: 3,
      Arguments: {0: 1, 1: 2, length: 2}// 函数上下文包含Arguments对象
    }
    outer: <GlobalExectionContext>, // 指向上层上下文
    this: <global object>
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
    }
    outer: <GlobalExectionContext>,
    this: <global object>test
  }
}
//当 test 函数执行完后,将返回值更新到变量 c

4 . 上下文栈

  • 执行栈(执行上下文栈),在其他编程语言中也叫调用栈,是一个后进先出的结构。它用来存储代码执行过程中创建的所有执行上下文。

  • 当 JavaScript 引擎执行你的代码时,它会创建一个全局执行上下文并且将它推入当前的执行栈。当代码执行流进入函数时,函数的上下文被推到一个上下文栈顶。在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。ECMAScript程序的执行流就是通过这个上下文栈进行控制的。

let a = 'Hello World!';

function first() {
  console.log('1');
  second();
  console.log('11');
}
function second() {
  console.log('2');
}
first();
console.log('全局上下文');

上述代码上下文栈变化过程

5.png

5. 作用域链

上下文中的代码在执行的时候,会创建变量对象的一个作用域链(scope chain)。这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。代码正在执行的上下文的变量对象始终位于作用域链的最前端

var color = "blue"; 
function changeColor() { 
  let anotherColor = "red"; 
  function swapColors() { 
    let tempColor = anotherColor; 
    anotherColor = color; 
    color = tempColor; 
    // 这里可以访问 color、anotherColor 和 tempColor 
  } 
 // 这里可以访问 color 和 anotherColor,但访问不到 tempColor 
 swapColors(); 
 console.log(anotherColor);
} 
// 这里只能访问 color 
changeColor();

console.log(color);

6.png

图中矩形表示不同的上下文。上下文之间的连接是线性的、有序的。每个上下文都可以通过作用域链到上一级上下文中去搜索变量和函数,但任何上下文都不能到下一级上下文中去搜索。

  • swapColors()局部上下文的作用域链中有 3 个对象:swapColors()的变量对象、changeColor()的变量对象和全局变量对象。swapColors()的局部上下文首先从自己的变量对象开始搜索变量和函数,搜不到就去搜索上一级变量对象。

  • changeColor()上下文的作用域链中只有 2 个对象:它自己的变量对象和全局变量对象。因此,它不能访问 swapColors()的上下文。

参考文章参考文章

关于js中的执行上下文

理解 JavaScript 中的执行上下文

JavaScript高级程序设计(第4版)