理解JS执行上下文以及执行堆栈

什么是执行上下文?

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

执行上下文的类型有哪些?

JavaScript中有三种类型的执行上下文

  • 全局执行上下文 - 这是默认或基本执行上下文。不在任何函数内的代码位于全局执行上下文中。它执行两件事:它创建一个全局对象,它是一个窗口对象(在浏览器的情况下window,node环境global),并将其值设置为等于全局对象。程序中只能有一个全局执行上下文。
  • 函数执行上下文 - 每次调用函数时,都会为该函数创建一个全新的函数执行上下文。每个函数都有自己的函数执行上下文,在调用或调用函数时会创建函数执行上下文。可以有任意数量的函数执行上下文。每当创建一个新的函数执行上下文时,它都会按照定义的顺序执行一系列步骤,我将在本文后面讨论。
  • Eval函数执行上下文 - 在eval函数内执行的代码也有自己的Eval函数执行上下文,但由于JavaScript开发人员通常不使用eval,所以我在此不再讨论。

执行堆栈

执行堆栈,在其他编程语言中也称为“调用堆栈”(calling stack),是具有LIFO(后进先出)结构的堆栈,用于存储在代码执行期间创建的所有执行上下文。 当JavaScript引擎首次遇到你的代码时,它会创建一个全局执行上下文并将其推送到当前执行堆栈。每当引擎找到函数调用时,它都会为该函数创建一个新的函数执行上下文,并将其推送到堆栈的顶部。 引擎执行其位于堆栈顶部的函数执行上下文。当此函数完成时,其执行上下文将从堆栈中弹出,控件将到达当前堆栈中它下面的执行上下文。 让我们通过下面的代码示例来理解这一点:

let a = 'Hello World!';
function first() {
  console.log('Inside first function');
  second();
  console.log('Again inside first function');
}
function second() {
  console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');

上述代码执行堆栈的图解
上述代码的图解
当上述代码在浏览器中加载时,Javascript引擎会创建一个全局执行上下文并将其推送到当前执行堆栈。当遇到对first()的调用时,Javascript引擎为该函数创建一个新的执行上下文,并将其推送到当前执行堆栈的顶部。

当从first()函数中调用second()函数时,Javascript引擎为该函数创建一个新的执行上下文,并将其推送到当前执行堆栈的顶部。当second()函数完成时,它的执行上下文从当前堆栈中弹出,控件到达它下面的执行上下文,即first()函数执行上下文。

first()完成时,其执行堆栈将从堆栈中移除,并且控制将到达全局执行上下文。一旦执行了所有代码,JavaScript引擎就会从当前堆栈中删除全局执行上下文。

如何创建执行上下文?

到目前为止,我们已经看到JavaScript引擎如何管理执行上下文。现在让我们了解JavaScript引擎如何创建执行上下文。
执行上下文分两个阶段创建:1)创建阶段和2)执行阶段。

创建阶段

在创建阶段发生以下事情:

  1. LexicalEnvironment创建
  2. VariableEnvironment创建

因此,执行上下文可以在概念上表示如下:

ExecutionContext = {
  LexicalEnvironment = <ref. to LexicalEnvironment in memory>,
  VariableEnvironment = <ref. to VariableEnvironment in  memory>,
}

Lexical Environment

每个 Lexical Environment都由三个组成部分:

  1. Environment Record (记录环境变量)
  2. Reference to the outer environment, (记录外部环境, 例如在浏览器中一个函数在全局下定义,外部环境就是window)
  3. This binding.

代码示例

扫描二维码关注公众号,回复: 6161789 查看本文章
const person = {
  name: 'peter',
  birthYear: 1994,
  calcAge: function() {
    console.log(2018 - this.birthYear);
  }
}
person.calcAge(); 
// 'this' refers to 'person', because 'calcAge' was called with 'person' object reference
const calculateAge = person.calcAge;
calculateAge();
// 'this' refers to the global window object, because no object reference was given

抽象地说,Lexical Environment在伪代码中看起来像这样

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
    }
    outer: <null>,
    this: <global object>
  }
}
FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
    }
    outer: <Global or outer function environment reference>,
    this: <depends on how function is called>
  }
}
Variable Environment

它也是一个Lexical Environment,其EnvironmentRecord包含由此执行上下文中的VariableStatements创建的绑定。 如上所述,Variable Environment也是一个词汇环境,因此它具有上面定义的词法环境的所有属性和组件。 在ES6中,LexicalEnvironment组件和VariableEnvironment组件之间的一个区别是前者用于存储函数声明和变量(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);

执行上述代码时,JavaScript引擎会创建一个全局执行上下文来执行全局代码。因此,在创建阶段,全局执行上下文将如下所示:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      a: < uninitialized >,
      b: < uninitialized >,
      multiply: < func >
    }
    outer: <null>,
    ThisBinding: <Global Object>
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      c: undefined,
    }
    outer: <null>,
    ThisBinding: <Global Object>
  }
}

在执行阶段,完成变量赋值。因此,在执行阶段,全局执行上下文将看起来像这样:

GlobalExectionContext = {
LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      a: 20,
      b: 30,
      multiply: < func >
    }
    outer: <null>,
    ThisBinding: <Global Object>
  },
VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      c: undefined,
    }
    outer: <null>,
    ThisBinding: <Global Object>
  }
}

当遇到函数multiply(20,30)的调用时,会创建一个新的函数执行上下文来执行函数代码。因此,在创建阶段,函数执行上下文将如下所示:

FunctionExectionContext = {
LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      Arguments: {0: 20, 1: 30, length: 2},
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>,
  },
VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      g: undefined
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>
  }
}

在此之后,执行上下文将进入执行阶段,这意味着完成了对函数内部变量的赋值。因此,在执行阶段,函数执行上下文将如下所示:

FunctionExectionContext = {
LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      Arguments: {0: 20, 1: 30, length: 2},
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>,
  },
VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      g: 20
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>
  }
}

函数完成后,返回的值存储在c中。因此全局执行上下文得到了更新。之后,全局代码完成,程序结束。

你可能已经注意到let和const定义的变量在创建阶段没有任何与之关联的值,但var定义的变量设置为undefined。

这是因为,在创建阶段,代码就会扫描varfunction声明,而function声明完全存储在环境中,变量最初设置为undefined(如果是var)或保持未初始化( let和const的情况)

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

这就是我们所谓的变量提升

领个红包,小赞赏一下吧

翻译自Sukhjinder Arora的博客:
Understanding Execution Context and Execution Stack in Javascript

猜你喜欢

转载自blog.csdn.net/coderMozart/article/details/85265116
今日推荐