浅谈对JavaScript 中的执行上下文和执行栈的理解

大家好,金三银四马上也快到了,总听说行情不好,面试不好面,不过好像也没什么太大关系,该换新工作就换,只要准备充分还怕它什么行情不好。笔者呢最近也有想法所以再回顾JavaScript知识时,又看到了JavaScript的执行上下文

​那么这篇文章呢一小部分内容是我自己的一些理解。​

大部分内容来自​​[译] 理解 JavaScript 中的执行上下文和执行栈​

原文地址:​​Understanding Execution Context and Execution Stack in Javascript​

例题

大家先来看一道较为简单的题,看下是否能看出来结果

var a = 10;
function fn(b) {
b = 20;
console.log(a, b);
}
function fn1() {
a = 100;
fn(a);
}
fn(200); //输出结果
fn1(); // 输出结果

大家可以看出来输出结果是什么吗?

如果你已经算出来的话,那么说明你对执行上下文还是有一些理解的,欢迎继续往下看加深印象

如果你没算出来或者输出结果与你算的不相符,那也先不要着急,先看下边内容,看完后再回来算

执行上下文

概念

大家都知道,JavaScript代码的在运行的时候都是自上而下按顺序执行的,但是呢实际并非是一行一行的执行,那大家有没有了解过它在执行代码的时候做过哪些准备,做过哪些事情,比如代码解析、分配内容都是在哪处理的,那这个地方呢就是执行上下文,是准备工作的所在环境

执行上下文类型

执行上下文呢有三种类型,分别是

  • 全局执行上下文
  • 函数执行上下文
  • 还有就是eval函数执行上下文

那么我们继续,执行上下文呢是在代码编译阶段创建的,来看看执行上下文的生命周期

执行上下文生命周期

  • 创建阶段
  • 执行阶段
创建阶段

执行上下文的创建阶段具体做了什么事呢,又分为三部分

ExecutionContext = {
ThisBinding = <this value>,
LexicalEnvironment = { ... },
VariableEnvironment = { ... },
}
确定this指向

在全局执行上下文中,this指向的是全局对象

在函数执行上下文中,this指向取决于该函数是如何被调用的

看下这个demo

const obj = {
fn: function(){
console.log(this)
}
}

obj.fn(); //fn: f();

const func = obj.fn;

func(); // Window
词法环境

​官方的 ES6​​ 文档把词法环境定义为

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

简单来说词法环境是一种持有标识符—变量映射的结构。(这里的标识符指的是变量/函数的名字,而变量是对实际对象[包含函数类型对象]或原始数据的引用)。

现在,在词法环境的内部有两个组件:(1) 环境记录器和 (2) 一个外部环境的引用

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

词法环境有两种类型:

  • 全局环境(在全局执行上下文中)是没有外部环境引用的词法环境。全局环境的外部环境引用是 null。它拥有内建的 Object/Array/等、在环境记录器内的原型函数(关联全局对象,比如 window 对象)还有任何用户定义的全局变量,并且 ​​this​​的值指向全局对象。
  • 函数环境中,函数内部用户定义的变量存储在环境记录器中。并且引用的外部环境可能是全局环境,或者任何包含此内部函数的外部函数。

环境记录器也有两种类型(如上!):

  1. 声明式环境记录器存储变量、函数和参数。
  2. 对象环境记录器用来定义出现在全局上下文中的变量和函数的关系。

简而言之,

  • 全局环境中,环境记录器是对象环境记录器。
  • 函数环境中,环境记录器是声明式环境记录器。

注意 — 对于函数环境声明式环境记录器还包含了一个传递给函数的 ​​arguments​​ 对象(此对象存储索引和参数的映射)和传递给函数的参数的 length

抽象地讲,词法环境在伪代码中看起来像这样:

GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 在这里绑定标识符
}
outer: <null>
}
}

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

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

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

在 ES6 中,词法环境组件和变量环境的一个不同就是前者被用来存储函数声明和变量(​​let​​ 和 ​​const​​)绑定,而后者只用来存储 ​​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>
}
}

注意 — 只有遇到调用函数 ​​multiply​​ 时,函数执行上下文才会被创建。

可能你已经注意到 ​​let​​ 和 ​​const​​ 定义的变量并没有关联任何值,但 ​​var​​ 定义的变量被设成了 ​​undefined​​。

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

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

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

执行阶段

这是整篇文章中最简单的部分。在此阶段,完成对所有这些变量的分配,最后执行代码。

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

执行栈

那根据上述执行上下文的理解,那我们知道在执行代码中会有很多的执行上下文,那么执行上下文是怎么确定执行顺序的。

执行上下文存放的位置就是在执行上下文栈,也叫调用栈。具有LIFO(Last In First Out后进先出,也就是先进后出)的特性。

那我们来看下之前的例题,来分析下

var a = 10;
function fn(b) {
b = 20;
console.log(a, b);
}
function fn1() {
a = 100;
fn(a);
}
fn(200); //输出结果
fn1(); // 输出结果
  1. 首先进入全局执行环境,创建全局执行上下文环境并加入栈中
  2. fn()函数被调用,进入对应的函数执行环境,创建函数执行环境并加入栈
  3. 执行 console.log(a, b);代码
  4. console.log(a, b);代码出栈
  5. fn()函数执行完毕后出栈
  6. fn1()函数被调用,进入对应的函数执行环境,创建函数执行环境并加入栈
  7. 继续fn()函数被调用,进入对应的函数执行环境,创建函数执行环境并加入栈
  8. 执行 console.log(a, b);代码
  9. console.log(a, b);代码出栈
  10. fn()函数执行完毕后出栈
  11. fn1()函数出栈
  12. 全局执行上下文出栈

浅谈对JavaScript 中的执行上下文和执行栈的理解

题解

那我们再来分析下例题的答案

var a = 10;
function fn(b) {
b = 20;
console.log(a, b);
}
fn(200);

在执行fn函数时,此fn活动对象为

AO : {
a: 10,
b: 20,
arguments: {0 : 20, length:0}
}

所以此时输出结果为10,20

继续看

var a = 10;
function fn(b) {
b = 20;
console.log(a, b);
}
function fn1() {
a = 100;
fn(a);
}
fn1();

在执行fn1函数时,此fn1活动对象为

AO : {
a: 100,
fn: reference to function fn(){}
arguments: {length: 0}
}

在继续执行fn函数时,此fn活动对象为

AO : {
a: 100,
b: 20,
arguments: {0 : 20, length:0}
}

所以此时输出结果为100,20

结语

如果感觉此文的大屏数据交互方式对你帮助的话,请不吝点个赞???,支持一下

© 版权声明

相关文章