【JavaScript】这次深入理解一下执行上下文

前言

执行上下文JavaScript中是很重要的概念,几乎牵涉到了方方面面的知识,理解了执行上下文能够很好的帮助理解JavaScript的运行机制,很多问题都能迎刃而解。然而,执行上下文是个很抽象的东西,很难被观察到,因此这一次我们就来一起学习一下JavaScript中的执行上下文

了解一下执行上下文

在学习之前,我们首先得对执行上下文有一定的概念和理解。

什么是执行上下文

简而言之,执行上下文就是当前 JavaScript 代码被解析和执行时所在环境的抽象概念, JavaScript 中运行任何的代码都是在执行上下文中运行。

也就是说,我们的代码实际上都是运行在上下文中的,而我们之前介绍的作用域和提升以及以后会介绍的this等等知识点都是建立于执行上下文的基础上的,因此在介绍这些概念的同时难免会提到上下文的概念,接下来的篇幅就让我们一起来感受一下执行上下文的魅力吧。

执行上下文的类型

执行上下文的类型可以分为三种,分别是全局执行上下文函数执行上下文Eval 函数执行上下文,我们来分别介绍一下:

  • 全局执行上下文
    全局执行上下文有且仅有一个,会在首次运行js的时候创建出来并添加到执行栈中,在退出浏览器的时候移除。
  • 函数执行上下文
    函数执行上下文可以存在无数个,当我们调用函数的时候就会创建函数执行上下文,但只有函数执行结束之后,这个执行上下文才会被移除,如果在函数体中调用了其他函数,那么其他函数的上下文会继续添加到执行栈,关于执行栈的知识我们接下来会讲到。
  • Eval 函数执行上下文
    Eval 函数执行上下文是指调用Eval时,会创建一个新的执行上下文,这种用法很少用且非常不推荐。

执行上下文的执行栈

之前我们在介绍作用域时提到,内部函数可以访问外部函数的变量,这些变量存放在哪里?为什么会有作用域链?

答案就在于JS执行代码期间,执行上下文存放着我们需要的关键信息,而这些执行上下文则存放在一种具有后进先出(LIFO)结构的执行栈,或者叫做调用栈中。

我们这里先看个例子:

function outer(){
    console.log(1);
    inner();
}
function inner(){
    console.log(2);
}
outer();

我们通过画图解释下这段代码的执行栈,首先当我们还未执行这段代码的时候,会存在一个全局执行上下文

image

我们上面介绍过了,函数执行上下文是在调用函数的时候创建的,因此当代码执行到:

outer();

此时我们会创建一个outer函数的执行上下文,放入到执行栈中,就像这样:
[图片上传失败...(image-3b4aab-1606651259278)]

outer什么时候移出去呢?别急,我们在执行outer函数的过程中又遇到了inner函数的调用:

function outer(){
    console.log(1);
        inner();//调用inner函数
    }

这时候outer函数相当于没有执行结束,我们又创建了inner函数的执行上下文,就像这样:

image

层层套娃,还好inner函数体中没有再调用其他函数了,很快就执行结束了,我们会把inner函数的上下文从执行栈中移除释放掉,调用栈又变回了我们上一张图的状态:
[图片上传失败...(image-ae6ca1-1606651259279)]

释放掉inner函数的上下文以后,我们又回到了outer函数的上下文中了,如果此时函数体中又调用了其他函数,那么我们会继续创建其他函数的上下文,层层套娃,但这里我们的outer函数已经执行结束了,因此我们把outer函数的执行上下文也从执行栈中移除,最终我们的调用栈又回归到了初始状态:

image

全局上下文在运行的过程中会一直存在,且一直处于执行栈的底部,直到结束运行或退出浏览器才会被移除。

总结一下执行上下文的执行栈特点:

  • 首次运行脚本时会创建全局上下文,在退出时移除
  • 当调用函数时会创建函数执行上下文,并添加到栈顶,此时的执行上下文即栈顶的上下文
  • 当函数执行结束时,会从栈顶移除这个执行上下文,此时的执行上下文指向最新栈顶的上下文

深入执行上下文

刚刚我们了解了执行上下文类型管理,但没有介绍到,执行上下文创建执行的过程是什么?执行上下文包括了那些东西呢?我们来探索一下:

执行上下文组成

执行上下文可以看做三部分组成,分别是this的指向词法环境(LexicalEnvironment)变量环境(VariableEnvironment),我们也可以用伪代码表示为:

ExecutionContext = {  
  ThisBinding = ,  
  LexicalEnvironment = { ... },  
  VariableEnvironment = { ... },  
}

我们来分别了解一下各个部分功能。

this的指向

this的指向包括在这个上下文中this 的值,也被称为 This Binding。由于在JavaScript中,this的指向是动态的,也就是在创建执行上下文过程中确定this的值,并存放在This Binding中,关于this的绑定规则比较复杂,这里先不展开了,之后可能会单独介绍。

词法环境

词法环境的定义

词法环境在官方ES6文档中的定义是这样的:

A Lexical Environment is a specification type used to define the association of Identifiers to specific variables and functions based upon the lexical nesting structure of ECMAScript code.

翻译过来就是,词法环境是一种规范类型,用于根据ECMAScript代码的词法嵌套结构定义标识符与特定变量和函数的关联。简单的说就是,词法环境是一个包含标识符变量映射的结构,你可以在这里通过映射找到你需要的对象或者函数。 标识符是一个字符串名称,而变量则是指实际存储的内容,通过标识符字符串和词法环境这个映射结构,我们可以拿到我们需要的变量值。

词法环境的组成

词法环境环境记录和可能为空的对外部词法环境的引用组成。通常,词法环境与ECMAScript代码的一些特定语法结构相关联,比如FunctionDeclaration、BlockStatement或TryStatement的Catch子句,并且每次执行这些代码时都会创建一个新的词法环境

是不是觉得和作用域全局作用域函数作用域块级作用域很像?就是因为词法环境就是JS中作用域的实现机制,所以词法环境有时候也被称作词法作用域

不小心扯远了,我们先看看词法环境的组成内容,需要注意的是,词法环境可以分为全局环境下和函数环境下两种类型,但包含的内容类型都是一致的,都包括了环境记录对外部环境的引用

  • 环境记录
    存储变量和函数声明的实际位置,我们可以在这里获取到我们想要的变量和函数值。

在全局环境下,主要记录的是全局对象window上的属性和方法,以及用户定义的全局变量。
在函数环境下,主要记录的则是在函数中定义的变量,除此之外,还包含了一个 arguments 对象,该对象包含了索引和传递给函数的参数之间的映射以及传递给函数的参数的长度(数量)

  • 对外部环境的引用
    引用的外部环境的值,通过外部环境的引用值可以形成链式查找,层层向外需要搜索变量。

在全局环境下,对外部环境的引用值为null
在函数环境下,对外部环境的引用值取决于函数定义的位置,可能为外部函数环境或者全局环境

词法环境的伪代码表示

我们用伪代码表示一下全局环境函数环境下的词法环境吧:

  • 全局环境
GlobalExectionContext = {  
  LexicalEnvironment: {  
    EnvironmentRecord: {  //环境记录
      Type: "Object",  
      ...       // 全局环境下的变量和函数声明
      }
    outer:   //全局环境下,对外部环境的引用为null
  }  
}
  • 函数环境
FunctionExectionContext = {  
  LexicalEnvironment: {  
    EnvironmentRecord: {  //环境记录  
      Type: "Declarative",  
      ...       // 函数环境下的变量和函数声明
    outer:   // 函数环境下,对外部函数的引用取决于函数定义的位置
  }  
}

由于函数环境下的环境记录还包括arguments对象,这里我们举个例子:

function test(a, b) {  
  console.log(arguments); //Arguments: {0: 2, 1: 3, length: 2},
}  
test(1, 2);

可以看到,我们可以直接在test函数中输出arguments对象,我们用伪代码表示一下此时的词法环境:

FunctionExectionContext = {  
  LexicalEnvironment: {  
    EnvironmentRecord: {  //环境记录  
      Type: "Declarative",  
      arguments: {0: 2, 1: 3, length: 2},
    outer:   // 这里test函数定义在全局,因此对外部环境的引用值为全局
  }  
}

关于词法环境,我们已经花了大量的篇幅和例子去理解,但其实还没有完全详细的解释完所有的细节,我们在接下来的变量环境中做一些补充和对比理解。

变量环境

变量环境其实也是一个词法环境,因此它具有上面定义的词法环境的所有属性,那么为什么要把变量环境词法环境中分离出来呢?这是由于他它们的环境记录的类别差异导致的。
我们刚刚说了,词法环境是一个包含标识符变量映射的结构 ,而环境记录是其中存储变量和函数声明的实际位置,那么具体是怎么记录的呢?这就是变量环境词法环境的差别所在,我们分别介绍一下这个过程:

变量环境中的环境记录

这里的环境记录只用于var声明的标识符和变量的绑定。

  1. 全局环境会将环境记录中所有的标识符绑定到window对象的同名属性上
    此时我们执行var name ='window',实际上就是在执行window.name ='window'
  2. 全局环境会将window对象的所有属性名绑定到环境记录中的同名标识符上
    我们先执行window.name="2",会发现此时我们可以直接执行console.log(name)输出window.name的值。
  3. 如果标识符已经绑定了window上的原有属性上,那么该变量就是对应属性值,否则的话就实例化变量并赋值为undefined(变量提升)
  4. 如果标识符已经存在了,重复的var声明将被无视

看完了变量环境,我们再来看看词法环境

词法环境中的环境记录

这里的环境记录用于存储函数声明,以及letconst声明的标识符变量的绑定。

  1. 将所有非var声明的标识符实例化,但不初始化,让变量处于uninitialized状态(不可变量提升)。也就是说内存中已经为变量预留出空间,但是还没有和对应的标识符建立绑定关系
  2. 当执行到声明语句的时候,才进行初始化以及后续的赋值操作
  3. 不允许重复声明,如果已存在则报错,声明的标识符也不可以用var重新声明。
举个例子

我们来看个例子,并通过伪代码的形式结合看看两者的形式:

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

function multiply(e, f) {  
 var g = 20;  
 return e * f * g;  
}

c = multiply(20, 30);

当执行c = multiply(20, 30)的时候会创建出执行上下文,我们用伪代码表示一下此时的执行上下文

  • 全局环境
GlobalExectionContext = {

  ThisBinding: ,

  LexicalEnvironment: {  //let和const以及函数声明存储的地方
    EnvironmentRecord: {  
      Type: "Object",  
      a: < uninitialized >,  
      b: < uninitialized >,  //a和b只进行了实例化,未初始化
      multiply: < func >  
    }  
    outer:   
  },

  VariableEnvironment: {  //var声明存储的地方
    EnvironmentRecord: {  
      Type: "Object",  
      // 标识符绑定在这里  
      c: undefined,  //c初始化为了undefined
    }  
    outer:   
  }  
}
  • 函数环境
FunctionExectionContext = {  
   
  ThisBinding: ,

  LexicalEnvironment: {  //let和const以及函数声明存储的地方
    EnvironmentRecord: {  
      Type: "Declarative",  
      // 标识符绑定在这里  
      Arguments: {0: 20, 1: 30, length: 2},  
    },  
    outer:   
  },

  VariableEnvironment: {  //var声明存储的地方
    EnvironmentRecord: {  
      Type: "Declarative",  
      g: undefined  //c初始化为了undefined
    },  
    outer:   
  }  
}

执行上下文的创建和执行

我们花了很大的篇幅分析了执行上下文,最后这个创建和执行的过程就很简单了:

创建阶段

  1. 确定this的值即This Binding
  2. LexicalEnvironment(词法环境)组件被创建
  3. VariableEnvironment(变量环境)组件被创建

执行阶段

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

大功告成啦~

总结

这篇文档由浅入深的讲解了一下执行上下文的相关知识,知识点非常多而且复杂,所以如果有理解错误的地方或不足之处,希望大家能指点帮助,我会很高兴的(* ̄︶ ̄)。

写在最后

  1. 很感谢你能认真的看到这里,如果觉得这篇文章对你有帮助不妨点个赞支持一下,感谢大家~
  2. 以后会陆续更新更多文章和知识点,感兴趣的话可以关注一波~
    可以看看其他相关知识点的介绍博客,和本篇结合说不定会对你有所帮助哦:
  • 【JavaScript】有趣的作用域和提升
  • 【JavaScript】来好好盘一盘闭包!

参考文章

  • 理解JavaScript执行上下文
  • 精读Javascript系列(二)环境记录与词法环境

你可能感兴趣的:(【JavaScript】这次深入理解一下执行上下文)