js - 执行上下文【附带作用域和执行上文的区别】

2,876 阅读10分钟

前置内容

js - 作用域链

作用域:负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。

当前代码运行的环境,可访问的变量以及作用域链上的变量环境对象。


什么是执行上下文

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


执行上下文是一种代码运行时的场景,环境的概念。就好比中国文化博大精深。可能同一句话在不同的场景下,说的意义和意识是两种不同的。

执行上下文的类型

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

执行栈

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

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

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

怎么创建执行上下文?

  1. this 值的决定,即我们所熟知的 This 绑定
  2. 创建词法环境组件。
  3. 创建变量环境组件。

词法环境 指定一个词法环境对象,用于解析该执行环境内的代码创建的标识符引用。
变量环境 指定一个词法环境对象,其环境数据用于保存由该执行环境内的代码通过 变量表达式函数表达式 创建的绑定。
This绑定 指定该执行环境内的 ECMA 脚本代码中 this 关键字所关联的值。

其中执行环境的词法环境和变量环境组件始终为 词法环境 对象。当创建一个执行环境时,其词法环境组件和变量环境组件最初是同一个值。在该执行环境相关联的代码的执行过程中,变量环境组件永远不变,而词法环境组件有可能改变

词法环境

词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符和具体变量和函数的关联。一个词法环境由环境记录器和一个可能的引用外部词法环境的空值组成。


简单来说词法环境是一种持有标识符—变量映射的结构。(这里的标识符指的是变量/函数的名字,而变量是对实际对象[包含函数类型对象]或原始数据的引用)。
现在,在词法环境的内部有两个组件:(1) 环境记录器和 (2) 一个外部环境的引用。

  • 环境记录器是存储变量和函数声明的实际位置。
  • 外部环境的引用意味着它可以访问其父级词法环境(作用域)。


环境记录器也有两种类型(如上!):声明式环境记录器存储变量、函数和参数。对象环境记录器用来定义出现在全局上下文中的变量和函数的关系。简而言之,

  • 在全局环境中,环境记录器是对象环境记录器。
  • 在函数环境中,环境记录器是声明式环境记录器。
GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 在这里绑定标识符
    }
    outer: <null>
  }
}

FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 在这里绑定标识符
    }
    outer: <Global or outer function environment reference>
  }
}


变量环境


它同样是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。

如上所述,变量环境也是一个词法环境,所以它有着上面定义的词法环境的所有属性。

在 ES6 中,词法环境组件和变量环境的一个不同就是前者被用来存储函数声明和变量(let 和 const)绑定,而后者只用来存储 var 变量绑定

image.png

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>
  }
}



总结:

标识符解释:

所谓标识符,就是变量、函数、属性或函数参数的名称。标识符可以由一或多个下列字符组成:

  • 第一个字符必须是一个字母、下划线(_)或美元符号($);
  • 剩下的其他字符可以是字母、下划线、美元符号或数字。

标识符中的字母可以是扩展 ASCII(Extended ASCII)中的字母,也可以是 Unicode 的字母字符,如 À 和 Æ(但不推荐使用)。

执行上下文

  1. this 值的决定,即我们所熟知的 This 绑定
  2. 创建词法环境组件。
  3. 创建变量环境组件。

词法环境和变量环境的区别

  • 词法环境指定一个词法环境对象,用于解析该执行环境内的代码创建的标识符引用。
  • 变量环境指定一个词法环境对象,其环境数据用于保存由该执行环境内的代码通过 变量表达式函数表达式 创建的绑定。
  • This绑定指定该执行环境内的 ECMA 脚本代码中 this 关键字所关联的值。

其中执行环境的词法环境和变量环境组件始终为 词法环境 对象。 当创建一个执行环境时,其词法环境组件和变量环境组件最初是同一个值。在该执行环境相关联的代码的执行过程中,变量环境组件永远不变,而词法环境组件有可能改变

function test(a) {
  var a = 1;
  let b = 2;
	var name = '123';
  var c = {
  	name: 'rod'
  }
  c.name = 'rodchen'
  
  with(c) {
  	console.log(name)
  }
}

test(123)

通常词法环境会与特定的 ECMAScript 代码诸如 FunctionDeclaration,WithStatement 或者 TryStatement 的 Catch 块这样的语法结构相联系,且类似代码每次执行都会有一个新的语法环境被创建出来。

执行到with的时候,词法环境不同

图示


image.png



执行上下文和作用域的区别:

这一部分内容来源于成员问我的这个问题。


创建时间不同

  • 作用域是在函数创建的时候就已经创建
  • 执行上下文是在函数运行的时候创建

创建者不同

  • 作用域是词法分析创建,静态
  • 执行上下文由js引擎创建,动态

名次解释

  • 引擎:从头到尾负责整个Javascript程序的编译及执行过程
  • 编译器:引擎的好朋友之一,负责语法分析及代码生成
  • 作用域: 引擎的另一位好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限

二者之间的联系

举例

var a = 2; 编译器:将程序分解成词法单元,然后将词法单元解析成一个树结构。

  1. 对于var a, 编译器会访问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作用域的集合中生命一个新的变量,并命名为a;
  2. 接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理 a = 2 这个赋值操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作 a的变量。如果否,引擎就会使用这个变量;如果不是,引擎会继续查找该变量如果引擎最终找到了 a 变量,就会将 2 赋值给它。否则引擎就会抛出一个异常!

总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。


编译器名词解释: 

LHS(Left Hand Side) 左侧查询:LHS查询则是试图找到变量的容器本身,从而可以对其赋值

RHS(Right Hand Side): 右侧查询 RHS查询与简单地查找某个变量的值别无二致,RHS并不是真正意义上的“赋值操作的右侧”,更准确地说是“非左侧”,可以将RHS理解成retrieve his source value(取到它的源值)

function foo(a) {
    //存在隐式i步骤: a = 2;
    var b = a;
    return a + b;
}

var c = foo( 2 );

下面是相关对话:

  • 引擎: 作用域大哥,我需要对c进行LHS, 你知道吗?
  • 作用域:我知道,给你。
  • 引擎:我说作用域,我需要为 foo 进行RHS引用。你见过它吗?
  • 作用域:别说,我还真见过,编译器那小子刚刚声明了它。它是一个函数,给你。
  • 引擎:哥们太够意思了!好吧,我来执行一下 foo 。
  • 引擎:作用域,还有个事儿。我需要为 a 进行LHS引用,这个你见过吗?
  • 作用域:这个也见过,编译器最近把它声名为 foo 的一个形式参数了,拿去吧。
  • 引擎:大恩不言谢,你总是这么棒。现在我要把 2 赋值给 a 。
  • 引擎: 作用域,我需要对a进行LHS, 请把这个值给我好吗!
  • 作用域: Ok,我知道了,给你吧.
  • 引擎:作用域,我需要对a进行RHS, 我相信你肯定知道了.
  • 作用: 当然,给你好了。
  • 引擎: ok, 谢谢你了,现在我要给a赋值了.
  • 引擎:我还有一个步骤, 我需要对ab进行RHS.
  • 作用域: 恩,给你。
  • 引擎:执行代码return a + b; 得到function的结构之后执行赋值语句。

总结

执行上下文需要作用域的配合,不然无法完成工作。根据上面词法环境的描述,也可以看看出执行上下文对作用域的引用。执行上下文的词法环境可以理解为js作用域【如果有错,请指出】

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