JavaScript中的执行上下文?

473 阅读7分钟

「这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战」。

大家好,我是刘十一,今天是元宵节,祝大家元宵节快乐~

今天在了解执行上下文之前我们先看看 JavaScript 代码的执行分为哪些阶段吧。

一、JavaScript的执行步骤

JavaScript的执行分为,解释执行 两个阶段,这两个阶段所做的事并不一样。

1、解释阶段

(1)词法分析

(2)语法分析

(3)作用域规则确定

2、执行阶段

(1)创建执行上下文

  • 创建阶段
    • this 绑定
    • 创建词法环境组件
    • 创建变量环境组件
  • 执行阶段

(2)执行函数代码

(3)垃圾回收

综上,可得出,执行上下文是在JavaScript执行的执行阶段。

二、什么是执行上下文

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

举个例子,当执行到一个函数的时候,就会进行准备工作,这里的 “准备工作”,用个更专业一点的说法,就叫做 "执行上下文(execution context)"。

三、执行上下文的类型

1、全局执行上下文

任何 不在函数内部的 都是全局执行上下文,它首先会 创建一个全局的window对象,再 设置 this 的值 等于这个全局对象,并且,一个程序中只有一个全局执行上下文。

2、函数执行上下文

当一个函数 被 调用时,就会为该函数 创建 一个 新的 执行上下文,函数的上下文 可以有任意多个

3、eval 函数执行上下文

执行在eval 函数 中的代码 会有 属于他自己的执行上下文。

代码示例

eval() 函数会将传入的字符串当做 JavaScript 代码进行执行。

console.log(eval('2 + 2'));
// expected output: 4

console.log(eval(new String('2 + 2')));
// expected output: 2 + 2

console.log(eval('2 + 2') === eval('4'));
// expected output: true

console.log(eval('2 + 2') === eval(new String('2 + 2')));
// expected output: false

eval函数不常使用,此处不拓展介绍。

四、、执行上下文栈

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

JavaScript引擎 使用 执行上下文栈 来管理 执行上下文。

1、JavaScript 引擎执行脚本时的步骤

(1)当JavaScript 执行代码时,首先 遇到 全局变量,会 创建 一个全局执行上下文栈 并且 压入执行栈 中,

(2)每当 遇到一个 函数调用,就会 为该函数 创建一个新的 执行上下文压入栈顶

(3)引擎执行 位于执行上下文 栈顶 的函数

(4)当函数执行完毕之后执行上下文 从栈中 弹出,继续执行下一个上下文。

(5)当所有的 代码 都执行完毕之后,从栈中 弹出 全局执行上文。

执行上下文栈.jpg

2、代码示例

let a = 'Hello Execution context stack!';
function first() {
  console.log('Output in the first function');
  second();
  console.log('Again output in the first function');
}
function second() {
  console.log('Output in the second function');
}
first();
console.log('Output in the Global Execution Context');

//Output in the first function
//Output in the second function
//Again output in the first function
//Output in the Global Execution Context

四、、创建执行上下文

创建执行上下文有两个阶段:创建阶段执行阶段

1、创建阶段

(1)this 绑定
  • 在全局执行上下文中,this 指向 全局对象(window 对象)
  • 在函数执行上下文中,this 指向 取决于函数如何调用。
  • 如果它被一个引用对象调用,那么 this 会被设置成那个对象,
  • 否则 this 的值会被设置为 全局对象 或者 undefined 。
(2)创建词法环境组件
  • 词法环境是一种 标识符-变量映射 的数据结构

标识符 是指 变量 / 函数名,变量 是对 实际对象 (包含 函数类型对象) 或 原始数据的引用

  • 词法环境的内部有两个组件:环境记录器 和一个 外部环境的引用

环境记录器:存储 变量和函数声明 的实际位置

  • 声名式环境记录器用来 存储变量、函数和参数。
  • 对象环境记录器用来 定义 出现在全局上下文中的 变量和函数 的关系。
  • 在全局环境中,环境记录器就是对象环境记录器。
  • 在函数环境中,环境记录器就是声明式环境记录器。
  • 注意:对于函数环境,声明式环境记录器还包含了一个传递给函数的 arguments 对象(此对象存储索引和参数的映射)和 传递给函数的参数的length。

外部环境的引用:意味着它 可以访问 其父级 词法环境(作用域)

  • 词法环境有两种类型:全局环境(在全局执行上下文中) 和 在函数环境中

全局环境(在全局执行上下文中)是 没有外部环境引用的 词法环境,外部环境引用是 null,拥有内建的Object/Array 等,this 的值指向全局对象。

环境记录器内的原型函数(关联全局对象,比如 window 对象)还有用户自定义的全局变量。

在函数环境中,函数内部 用户定义的变量 存储在 环境记录器中,并且 引用的外部环境 可能是全局环境,或者是 任何包含此内部函数的外部函数。

1)词法环境在伪代码中看起来如下:

// 1、全局环境
GlobalExectionContext = {
  // 词法环境(环境记录器和外部环境的引用)
  LexicalEnvironment: {
    // 环境记录器
    EnvironmentRecord: {
      Type: "Object",
      // 在这里绑定标识符
    }
    // 外部环境的引用
    outer: <null>
  }
}

//2、函数环境
FunctionExectionContext = {
  // 词法环境
  LexicalEnvironment: {
    // 环境记录器
    EnvironmentRecord: {
      Type: "Declarative",
      // 在这里绑定标识符
    }
    // 外部环境的引用
    outer: <Global or outer function environment reference>
  }
}
(3)创建变量环境组件
  • 变量环境 也是一个词法环境,其环境记录器 持有 变量声明语句 在 执行上下文中 创建的 绑定关系,同时,它也有着上面定义的 词法环境的所有属性。
  • 在ES6 中,词法环境组件 和 环境变量的一个不同就是 前者 被用来存储函数声明和变量( let 和 const ) 绑定,而后者 只用来存储 var 变量绑定。

1)代码示例

看代码理解上述概念

let a = 10;
const b = 20;
var c;

function add(d, e) {
 var f = 5;
 return d + e + f;
}

c = add(30, 40);

2)执行上下文的伪代码

GlobalExectionContext = 
  ThisBinding: <Global Object>,
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 在此处绑定标识符
      a: < uninitialized >,
      b: < uninitialized >,
      add: < func >
    }
    outer: <null>
  },

  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 在此处绑定标识符
      c: undefined,
    }
    outer: <null>
  }
}

FunctionExectionContext = {
  ThisBinding: <Global Object>,
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 在此处绑定标识符
      Arguments: {0: 30, 1: 40, length: 2},
    },
    outer: <GlobalLexicalEnvironment>
  },

VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 在此处绑定标识符
      f: undefined
    },
    outer: <GlobalLexicalEnvironment>
  }
}

注意:

只有遇到调用函数 add 时,函数执行上下文才会被创建。

3)分析:

综上,我们可以看出,当 let 和 const 定义的变量并没有关联任何值(< uninitialized >),但 var 定义的变量被设成了 undefined。

  • 这是因为在创建阶段时,引擎检查代码找出变量和函数声明,虽然函数声明完全存储在环境中,但是变量最初设置为 undefined(var 情况下),或者未初始化(let 和 const 情况下)。

  • 这就是为什么我们可以在声明之前访问 var 定义的变量(虽然是 undefined),但是在声明之前访问 let 和 const 的变量会得到一个引用错误。

  • 这就是我们说的 变量声明提升。

2、执行阶段

此阶段会完成对变量的分配,最后执行完代码。

注意:

在执行阶段,如果是 JavaScript 引擎 不能在 源码声明的实际位置 找到 let 变量的值,它会被赋值为 undefined。

五、实际应用

比较下面两段代码,试述两段代码的不同之处
// A--------------------------
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();

// B---------------------------
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();

解析: 首先A、B两段代码输出返回的都是 "local scope",但是它们的执行上下文栈的变化不一样。

先看如下的调用栈模拟结果:

第一段代码

ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();  // 弹出
ECStack.pop();

第一段代码

ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();

原因:

因为JavaScript采用的是 词法作用域(lexical scoping),即,静态作用域,函数的作用域 基于 函数创建的位置,即,在函数定义的时候就决定了。

var value = 1;

function foo() {
    console.log(value);
}

function bar() {
    var value = 2;
    foo();
}

bar(); //1