likes
comments
collection
share

2.5k字带你构建JS执行上下文知识体系

作者站长头像
站长
· 阅读数 27

前言

本文字数2.5k字,请读者耐心看完,会有收获,先赞后看,养成习惯

执行上下文在JS进阶中非常重要的,其内部的原理涉及到JS很多特性,而执行上下文中有很多难以理解的概念,比较抽象,令我们难以下咽,所以本文将以通俗易懂的方式来带大家构建执行上下文的知识体系,下面将分别从概念分类创建执行销毁问题

2.5k字带你构建JS执行上下文知识体系

概念📚

我根据官方文档以及内部原理对于执行上下文的概念理解大概为:在我们的JS代码开始执行的时候抽象出来的一个隔离环境,该环境通过一些方式存储并控制变量,函数的访问权限。

执行上下文为什么那么重要,因为它的原理牵扯到了很多问题,比如为什么var会发生变量提升现象而letconst不会,又比如作用域是如何控制变量访问的,文章最后会一一为大家解答。

分类😶‍🌫️

上下文分为三种,分别为:全局上下文函数上下文Eval()函数上下文,下面将主要讨论全局上下文函数上下文

  • 全局上下文:全局上下文在最外层,也就会涉及到我们熟知的顶层对象(windowglobal
  • 函数上下文:每个函数都有自己的执行上下文,所以称为函数上下文,函数的执行会触发函数上下文的一些行为

上下文创建🔛

上下文创建我们所需要知道的是创建时机及其生命周期,生命周期包括this绑定,词法环境创建变量环境创建

以伪代码形式来表示上下文的抽象结构,并进一步理解上下文创建发生的三个过程

ExecutionContext = {
  ThisBinding = <this value>,
  LexicalEnvironment = { ... },
  VariableEnvironment = { ... },
}

一个上下文初始结构如上,ThisBindingthis绑定指向,LexicalEnvironment为词法环境对象,VariableEnvironment为变量环境对象,下文将具体介绍每个部分的伪代码的具体组成,以助于更好的理解

创建时机

不同类别上下文的创建时机也不同

  • 全局上下文:当脚本被执行,就会立刻创建全局上下文
  • 函数上下文:每当函数被执行,那么上下文会在执行前创建

this绑定

大家经常会用this但是关于this绑定是在哪个阶段进行的没有了解过,其实是在执行上下文创建的过程中将this指向它应该指向的对象,其中全局上下文和函数上下文创建过程中的this绑定不同

  • 全局上下文:其this会指向全局对象(window,global等等)
  • 函数上下文:其this会指向函数执行时的调用者(对象,全局或者其他)

其实大家对于this的机制很熟悉,所以这部分也可以很直接地理解,那么疑问就来了,箭头函数的创建上下文是什么类别的呢?

  • 首先箭头函数的上下文是函数上下文
  • 其次上下文是比较特殊的,它的上下文就是父级(函数or全局)的上下文

箭头函数上下文的特性导致了其箭头函数内部的this是其定义所在的对象,另外也能想明白为什么箭头函数无法使用arguments参数对象,关于箭头函数无法使用参数对象这个问题我们看完下面的词法环境创建后就会理解。

词法环境创建

关于词法环境(LexicalEnvironment对象),掘友只需要记住其作用是解析当前上下文中由constlet声明的变量,将标识符与对应变量或者是函数关联起来即可,上下文的创建于执行都会涉及到词法环境的变化,让我们带着作用往下学习!

词法环境是由一个环境记录器与一个外部环境引用构成

  • 环境记录器:存储变量和函数的实际位置
  • 外部环境引用:在词法环境中以属性值存储外部环境的引用地址,那么就可以访问父级环境的变量等一系列东西

从上面两个组成角度看创建阶段发生了什么,下面这是一个基本词法环境的伪代码(空)

LexicalEnvironment: {  
    EnvironmentRecord: {  
        
    }  
    outer: <null>  
},

LexicalEnvironment代表词法环境,EnvironmentRecord代表环境记录器,outer代表外部环境引用,我们使用let 或者 const声明的变量,会在创建词法环境的过程中解析,解析后并将其标识符存放在环境记录器中,以未初始化的形式保存,比如我声明了一个a变量,这个时候会在EnvironmentRecord存放标识符a,其值为< uninitialized >即未初始化。

LexicalEnvironment: {  
    EnvironmentRecord: {  
         Type: "Object",
         a: < uninitialized >,
    }  
    outer: <null>  
},

上面就是一个词法环境创建过程中发生了什么,我们回过头想起词法环境的作用:解析当前上下文中由constlet声明的变量,将标识符与对应变量或者是函数关联起来,那么创建阶段就是在解析声明后,存放标识符,而当前由于是letconst声明,并没有进行初始化,所以其值为< uninitialized >

另外词法环境分为两种,其应用场景不相同

  • 函数环境:函数执行上下文创建函数词法环境,其环境记录器为对象环境记录器,,除此之外其外部环境引用指向其父级上下文(可能是函数也可能是全局)。
  • 全局环境:全局执行上下文会创建全局词法环境,其环境记录器为声明式环境记录器,不包含arguments参数对象,由于没用父级,所以其外部环境引用null

变量环境创建

变量环境(VariableEnvironment对象)其实也是一种词法环境,它与上面的词法环境不同的是,其解析的是var声明变量,存储标识符与对应的引用,其创建过程发生的事情和词法环境差不多,但是关于初始值上有一些差别,如下:

如果我们使用var声明一个变量b那么会在变量环境创建的时候解析,存放在环境记录器中,但是其值为undefined

 VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      b: undefined,
    }
    outer: <null>
  }

完整结构

好了,我们现在能够将一个完整的上下文结构以伪代码形式表现出来

ExecutionContext = {
  ThisBinding = <this value>,
  LexicalEnvironment: {  
    EnvironmentRecord: {  
         Type: "Object",
         a: < uninitialized >,
    }  
    outer: <null>  
  },
   VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      b: undefined,
    }
    outer: <null>
  }
}

上下文的创建基本就是这样,不知道掘友有没有理解,如果没有理解可以反复阅读思考(我参考官方文档和各种文章学习了一个礼拜才算对上下文比较透彻),如果理解到位,那么就可以开始学习上下文执行都发生了什么。

上下文执行🚶

上下文的创建是在脚本或函数执行前,而执行过程中会进行执行栈和负责相关的行为

执行栈

栈是一种先进后出数据结构,我们的上下文在执行过程正是存储在栈中,我们称作执行栈。

function fn_1 () {
    fn_2();
}
function fn_2() {}
fn_1();

我拿上面的脚本来给大家描述这一过程,上面的脚本包含了三个上下文,分别是全局上下文,fn_1函数上下文,fn_2函数上下文

  • 脚本执行,创建全局上下文并推入栈底
  • fn_1()被触发,执行前创建fn_1函数上下文并推入栈,执行内部代码
  • fn_2()fn_1()执行中被触发,创建fn_2函数上下文并推入栈,此刻fn_2为栈顶,执行内部代码
  • fn_2()执行完毕,出栈
  • fn_1()执行完毕,出栈
  • 应用程序关闭,全局上下文出栈

赋值

在上下文的创建阶段,我们在词法环境内部的环境记录器存储了标识符,而在执行阶段,就会进行赋值,执行的伪代码如下

  LexicalEnvironment: {  
    EnvironmentRecord: {  
         Type: "Object",
         a: "猪痞恶霸",
    }  
    outer: <null>  
  },

销毁回收🗑️

在上下文弹出栈后不会立刻被销毁,想要了解销毁的内容可以查阅垃圾回收相关的知识,垃圾回收不是本文的重点。

带着知识看问题❔

通过上面我们已经大致掌握了执行上下文的原理,带着知识看问题,文章开头我提出了变量提升作用域问题,那么我们来一一解答。

变量提升问题

关于变量提升:使用var声明变量,在声明前调用为undefinedundefined就是我们熟悉的声明但未赋值,这种现象叫做变量提升,但是letconst禁止了这一行为,使用let所声明的变量一定需要在声明后使用。

console.log(bar_1) // undefined
console.log(bar) // Cannot access 'bar' before initialization
let bar = 2 
var bar_1 = 1

那么letconst是如何阻止变量提升的呢?

我们回到上下文创建这个过程中,词法环境和变量环境在创建过程中会解析不同形式声明的变量,词法环境的创建会解析letconst声明的变量并存入环境记录器对象中,并标记其并没有初始化,而变量环境的创建会解析var声明的变量存入环境记录器对象中,其值为undefined,这就是为什么使用var声明的变量会发生变量提升并且打印值为undefined

作用域问题

为什么能访问上层作用域中的变量,不能访问下层作用域中的变量,那么我们就需要思考我们内部是以什么形式访问上层作用域的

还记得我们词法环境中的外部环境引用outer吗,作为词法环境的一个组成部分,可以访问父级上下文的词法环境,也就可以访问到上层作用域的成员,而词法环境中并没有内部环境引用的组成,所以无法对子级词法环境进行一个访问。

最后◀️

本文参考了javascript高级程序设计,译文其他相关作者的参考文章,经过一个星期的学习理解,耗费三天时间总结出这篇,如有一些相关问题或者看法,欢迎各位掘友大佬在评论区留言,我们一起学习交流!✌️