了解Javascript中的执行上下文和执行栈

作者 : 开心源码 本文共6198个字,预计阅读时间需要16分钟 发布时间: 2022-05-13 共212人阅读

前言

本文3356字,阅读大约需要9分钟。

总括: 本文深入的讲解了Javascript中的执行上下文和执行栈。

  • 原文地址:Understanding Execution Context and Execution Stack in Javascript
  • 公众号:「前台进阶学习」,回复「666」,获取一揽子前台技术书籍

流水在碰究竟处时才会释放活力。

正文

假如你是或者者想成为一名Javascript开发者,那就必需要知道Javascript内部是如何执行的。正确的了解Javascript中的执行上下文和执行栈对于了解其它Javascript概念(比方变量提升,作用域,闭包等)至关重要。

正确的去了解Javascript执行上下文和执行栈将会是你成为一名更好的Javascript开发者。

不多废话,我们现在就开始:)

什么是执行上下文

简单的来说,执行上下文是一种对Javascript代码执行环境的一种笼统概念,也就是说只需有Javascript代码运行,那么它就肯定是运行在执行上下文中。

执行上下文的类型

Javascript一共有三种执行上下文:

  • 全局执行上下文。这是一个默认的或者者说基础的执行上下文,所有不在函数中的代码都会在全局执行上下文中执行。它会做两件事:创立一个全局的window对象(浏览器环境下),并将this的值设置为该全局对象,另外一个程序中只能有一个全局上下文。
  • 函数执行上下文。每次调用函数时,都会为该函数创立一个执行上下文,每一个函数都有自己的一个执行上下文,但注意是该执行上下文是在函数被调用的时候才会被创立。函数执行上下文会有很多个,每当一个执行上下文被创立的时候,都会按照他们定义的顺序去执行相关代码(这会在后面会说到)。
  • Eval函数执行上下文。eval函数中执行的代码也会有自己的执行上下文,但因为eval函数不会被经常用到,这里就不做探讨了。(译者注eval函数容易导致恶意攻击,并且运行代码的速度比相应的替代方法慢,由于不推荐使用)。

执行栈

执行栈,在其余编程语言中也被称为“调用栈”,这是一种后进先出(LIFO)的数据结构,被用来储存在代码运行阶段创立的所有的执行上下文。

当Javascript引擎(译者注:Javascript引擎是执行Javascript代码的解释器,一般被内嵌在浏览器中)开始执行你第一行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');

上述代码的执行栈

<div align=”center”>上述代码的执行栈</div>

当上述代码在浏览器中加载时,Javascript引擎首先创立一个全局执行上下文并将其压入执行栈中,而后碰到first()函数被调用,此时再创立一个函数执行上下文压入执行栈中。

second()函数在first()函数中被调用时,引擎再针对这个函数创立一个函数执行上下文将其压入执行栈中,second函数执行完毕后,对应的函数执行上下文被推出执行栈销毁,而后控制流程到下一个执行上下文也就是first函数。

first函数执行结束,first函数执行上下文也被推出,引擎控制流程到全局执行上下文,直到所有的代码执行完毕,全局执行上下文也会被推出执行栈销毁,而后程序结束。

执行上下文是如何创立的?

现在我们已经理解了Javascript引擎是如何去解决执行上下文的,那么,执行上下文是如何创立的呢?

执行上下文的创立分为两个阶段:

  1. 创立阶段;
  2. 执行阶段;

创立阶段

执行上下文是在创立阶段被创立的,创立阶段包括以下几个方面:

  1. 创立词法环境;
  2. 创立变量环境;

因而执行上下文可以笼统为下面的形式:

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

词法环境

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

词法环境(Lexical Environments)是一种规范类型,用于根据ECMAScript代码的词法嵌套结构来定义标识符与特定变量和函数的关联。词法环境由一个环境记录(Environment Record)和一个可能为空的外部词法环境(outer Lexical Environment)引用组成。

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

举个例子,看看下面的代码:

var a = 20;var b = 40;function foo() {  console.log('bar');}

上面代码的词法环境相似这样:

lexicalEnvironment = {  a: 20,  b: 40,  foo: <ref. to foo function>}

每一个词法环境由下面三部分组成:

  1. 环境记录;
  2. 外部环境引用;
  3. 绑定this;
环境记录

所谓的环境记录就是词法环境中记录变量和函数公告的地方。

环境记录也有两种类型:

  1. 公告类环境记录。顾名思义,它存储的是变量和函数公告,函数的词法环境内部就包含着一个公告类环境记录。
  2. 对象环境记录。全局环境中的词法环境中就包含的就是一个对象环境记录。除了变量和函数公告外,对象环境记录还包括全局对象(浏览器的window对象)。因而,对于对象的每一个新添加属性(对浏览器来说,它包含浏览器提供给window对象的所有属性和方法),都会在该记录中创立一个新条目。

注意:对函数而言,环境记录还包含一个arguments对象,该对象是个类数组对象,包含参数索引和参数的映射以及一个传入函数的参数的长度属性。举个例子,一个arguments对象像下面这样:

function foo(a, b) {  var c = a + b;}foo(2, 3);// argument 对象相似下面这样Arguments: { 0: 2, 1: 3, length: 2 }

译者注:环境记录对象在创立阶段也被称为变量对象(VO),在执行阶段被称为活动对象(AO)。之所以被称为变量对象是由于此时该对象只是存储执行上下文中变量和函数公告,之后代码开始执行,变量会逐步被初始化或者是修改,而后这个对象就被称为活动对象

外部环境引用

对于外部环境的引意图味着在当前执行上下文中可以访问外部词法环境。也就是说,假如在当前的词法环境中找不到某个变量,那么Javascript引擎会试图在上层的词法环境中寻觅。(译者注:Javascript引擎会根据这个属性来构成我们常说的作用域链)

绑定this

在词法环境创立阶段中,会确定this的值。

在全局执行上下文中,this值会被映射到全局对象中(在浏览器中,也就是window对象)。

在函数执行上下文中,this值取决于谁调用了该函数,假如是对象调用了它,那么就将this值设置为该对象,否则将this值设置为全局对象或者是undefined(严格模式下)。例如:

const person = {  name: 'peter',  birthYear: 1994,  calcAge: function() {    console.log(2018 - this.birthYear);  }}person.calcAge(); // 上面calcAge的 'this' 就是 'person',由于calcAge是被person对象调用的const calculateAge = person.calcAge;calculateAge();// 上面的'this' 指向全局对象(window),由于没有对象调用它,或者者说是window调用了它(window省略不写)

词法环境笼统出来相似下面的伪代码:

GlobalExectionContext = {  LexicalEnvironment: {    EnvironmentRecord: {      Type: "Object",        // 标识符在这里绑定    }    outer: <null>,    this: <global object>  }}FunctionExectionContext = {  LexicalEnvironment: {    EnvironmentRecord: {      Type: "Declarative",      // 标识符在这里绑定    }    outer: <Global or outer function environment reference>,    this: <depends on how function is called>   }}

变量环境

其实变量环境也是词法环境的一种,它的环境记录包含了变量公告语句在执行上下文中创立的变量和具体值的绑定关系。

如上所述,变量环境也是词法环境的一种,因而它具备词法环境所有的属性。

在ES6中,词法环境变量环境的不同就是前者用来存储函数公告和变量公告(letconst)绑定关系,后者只用来存储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",      // 标识符在这里绑定      a: < uninitialized >,      b: < uninitialized >,      multiply: < func >    }    outer: <null>,    ThisBinding: <Global Object>  },  VariableEnvironment: {    EnvironmentRecord: {      Type: "Object",      // 标识符在这里绑定      c: undefined,    }    outer: <null>,    ThisBinding: <Global Object>  }}

在执行阶段,将完成变量的赋值操作,因而在执行阶段全局执行上下文看起来会像下面这样:

GlobalExectionContext = {  LexicalEnvironment: {    EnvironmentRecord: {      Type: "Object",      // 标识符在这里绑定      a: 20,      b: 30,      multiply: < func >    }    outer: <null>,    ThisBinding: <Global Object>  },  VariableEnvironment: {    EnvironmentRecord: {      Type: "Object",      // 标识符在这里绑定      c: undefined,    }    outer: <null>,    ThisBinding: <Global Object>  }}

当调用multiply(20, 30)时,将为该函数创立一个函数执行上下文,该函数执行上下文在创立阶段像下面这样:

FunctionExectionContext = {    LexicalEnvironment: {    EnvironmentRecord: {      Type: "Declarative",      // 标识符在这里绑定      Arguments: { 0: 20, 1: 30, length: 2 },    },    outer: <GlobalLexicalEnvironment>,    ThisBinding: <Global Object or undefined>,  },    VariableEnvironment: {    EnvironmentRecord: {      Type: "Declarative",        // 标识符在这里绑定      g: undefined    },    outer: <GlobalLexicalEnvironment>,    ThisBinding: <Global Object or undefined>  }}

而后,执行上下文进入执行阶段,这时候已经完成了变量的赋值操作。该函数上下文在执行阶段像下面这样:

FunctionExectionContext = {    LexicalEnvironment: {    EnvironmentRecord: {      Type: "Declarative",      // 标识符在这里绑定      Arguments: { 0: 20, 1: 30, length: 2 },    },    outer: <GlobalLexicalEnvironment>,    ThisBinding: <Global Object or undefined>,  },    VariableEnvironment: {    EnvironmentRecord: {      Type: "Declarative",      // 标识符在这里绑定      g: 20    },    outer: <GlobalLexicalEnvironment>,    ThisBinding: <Global Object or undefined>  }}

函数执行完成后,返回值存储在变量c中,此时全局词法环境被升级。之后,全局代码执行完成,程序结束。

注意:你可能已经注意到上面代码,letconst定义的变量ab在创立阶段没有被赋值,但var公告的变量从在创立阶段被赋值为undefined

这是由于,在创立阶段,会在代码中扫描变量和函数公告,而后将函数公告存储在环境中,但变量会被初始化为undefined(var公告的情况下)和保持uninitialized(未初始化状态)(使用letconst公告的情况下)。

这就是为什么使用var公告的变量可以在变量公告之前调用的起因,但在变量公告之前访问使用letconst公告的变量会报错(TDZ)的起因。

这其实就是我们经常听到的变量公告提升。

注意:执行阶段,假如Javascript引擎找不到letconst公告的变量的值,也会被赋值为undefined

结论

如上,我们讲解了Javascript代码是如何执行的,尽管说成为一名优秀的Javascript开发者并不需要完全搞懂这些概念,但对上面的概念有深入的了解有助于我们去学习和了解其它概念,比方:变量公告提升,闭包,作用域链等。


能力有限,水平一般,欢迎勘误,不胜感激。

订阅更多文章可关注公众号「前台进阶学习」,回复「666」,获取一揽子前台技术书籍

前台进阶学习

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 了解Javascript中的执行上下文和执行栈

发表回复