头图

花点时间了解执行上下文

引言

当我们在浏览器中运行JavaScript代码时,浏览器会先创建一个全局执行上下文(Global Execution Context),然后逐行解析和执行代码。

执行上下文是JavaScript中非常重要的概念,它决定了代码的执行顺序和作用域链等重要信息。了解执行上下文的概念和工作原理,对于理解JavaScript的运行机制和调试错误非常有帮助。

在本文中,我们将深入探讨JavaScript的执行上下文,从而帮助读者更好地理解JavaScript的运行机制。

1、什么是执行上下文

一般来说,听到上下文这个东西,很自然想到了语文老师讲到的在上下文中找到相关联的段落和句子...

其实在JS中的上下文更多的是一个抽象的概念。它具体是指在当前执行环境中的变量、函数声明,参数(arguments),作用域链,this等信息

1.1、浏览器如何理解执行JavaScript

浏览器并不理解我们在应用中编写的高级JavaScript代码。代码需要被转换成浏览器和计算机能够理解的格式——机器码

浏览器在读取HTML时,如果遇到了<script> 标签或包含JavaScript代码的属性如onClick,会发送给JavaScript引擎

浏览器的JavaScript引擎会创造一个特殊的环境来处理这些JavaScript代码的转换和执行。这个特殊的环境被称为执行上下文

执行上下文包含当前正在运行的代码和有助于其执行的所有内容。在执行上下文运行期间,编译器解析代码,内存存储变量和函数,可执行的字节码生成后,代码执行。

实在不好理解,先入为主,将之想象成一个执行JS的容器

1.2、执行上下文

执行上下文JavaScript中非常重要的概念,它代表了代码执行时的环境。每当JavaScript引擎执行一段代码时,都会创建一个执行上下文。执行上下文包含了三个重要的组成部分:变量对象作用域链this值

  • 变量对象:是当前执行上下文中的变量、函数声明和函数参数的存储空间。
  • 作用域链:是当前执行上下文中所有父级执行上下文的变量对象的集合,它决定了当前执行上下文中变量的可访问性。
  • this值:代表当前函数的执行环境。

2、执行上下文有哪些类型呢

执行上下文

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

  • 全局执行上下文(GEC)

    任何不在函数内部的代码都在全局上下文中。一个程序中只会有一个全局执行上下文

  • 函数执行上下文(FEC)

    每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,它在函数被调用时创建。函数上下文可以有任意多个。

  • Eval函数执行上下文

    执行在eval函数内部的代码也会有它属于自己的执行上下文。eval不经常被使用到。

    🏄🏻 小知识

    eval()函数计算JavaScript字符串,并把它作为脚本代码来执行。

    如果参数是一个表达式,eval()函数将执行表达式。如果参数是Javascript语句,eval()将执行Javascript语句。

主要的还是全局执行上下文函数执行上下文

3、执行上下文的生命周期

在JavaScript中,执行上下文的生命周期可以分为三个阶段:创建阶段、执行阶段和销毁阶段。

3.1、创建阶段

在创建阶段,执行上下文首先与执行上下文对象(ECO)相关联。执行上下文对象存储了许多重要的数据,执行上下文中的代码在运行时会使用这些数据。创建阶段分三个步骤来定义和设置执行上下文对象的属性:

  • 创建变量对象(VO
  • 创建作用域链
  • 设置this关键字的值

3.1.1、创建变量对象(VO)

变量对象(VO)是一个在执行上下文中创建的类似于对象的容器,存储执行上下文中变量函数声明

GEC中,每当使用var关键字声明变量,VO就会添加一个指向该变量的属性,并将值设置为undefined。每当函数声明时,VO就会添加一个指向该函数的属性,并将这个属性存储在内存中。这就意味着在开始运行代码之前,所有函数声明就已经存储在VO中,并可以在VO中访问

但在FEC中并不创建VO,而是生成一个类数组对象,称为arguments对象,在下文称AO,包含传入函数的所有参数。

🏄🏻 小知识

这种将变量和函数声明存储在内存中优先于执行代码的过程被称为提升

3.1.2、创建作用域链

JavaScript中的作用域链是一个机制,决定了一段代码对于代码库中其他一些代码来说的可访问性

可以带着这样一些问题思考:

  • 一段代码可以在哪里访问?哪里不能访问?
  • 代码哪些部分可以被访问?哪些部分不能?

每一个函数执行上下文都会创建一个作用域,作用域相当于是一个空间/环境,变量和函数定义在这个空间里,并且可以通过一个叫做作用域查找的过程访问。如果函数被定义在另一个函数内部,处在内部的函数可以访问自己内部的代码以及外部函数(父函数)的代码。这种行为被称作词法作用域查找。但外部函数并不能访问内部函数的代码。

🏄🏻 小知识

作用域的概念就引出了JavaScript另一个相关的现象——闭包。闭包指的是内部函数永远可以访问外部函数中的代码,即便外部函数已经执行完毕。

JavaScript引擎一路向上遍历执行上下文直至解析处在函数内部触发的变量和函数的概念就叫作用域链

3.1.3、设置this关键字的值

JavaScriptthis关键字指的是执行上下文所属的作用域。一旦作用域链被创建,JS引擎就会初始化this关键字的值。

全局上下文中的this值:

GEC(所有函数和对象之外)中,this指向全局对象——window对象。同时,由var关键字初始化的函数声明和变量会被作为全局对象(window对象)的方法或者属性。

在任何函数外声明的变量和函数,如下:

var name = "jack"; 

function getName() { 
  console.log('hello') 
};

与下方的写法是一致的:

window.name = "jack"; 

window.getName = () => { 
  console.log('hello') 
};

GEC中的函数和变量会被当作window对象的方法和属性。

函数中的this

FEC中,并没有创建this对象,而是能够访问this被定义的环境。

在函数内部访问this的属性,示例:

var msg = "hello world!"; 

function printMsg() { 
  console.log(this.msg); 
} 

printMsg(); // hello world!

🏄🏻 小知识

在对象中,this关键字并不指向GEC,而是指向对象本身。

引用对象中的this如同引用:

对象.定义在对象内部的属性或方法;

示例代码:

var msg = "hello world!"; 
const Obj = {
    msg = "no hello world!"; 
    printMsg() { console.log(this.msg); } 
}

Obj.printMsg(); // no hello world!

出现上述的情况,函数可以访问的this关键字的值是定义其的对象Obj,而不是全局对象。

this关键字的值设置后,执行上下文对象的所有属性就定义完成,创建阶段结束,JS引擎就进入到执行阶段。

3.2、执行阶段

执行上下文创建阶段之后就是执行阶段了,在这一阶段代码执行真正开始。创建阶段之后,VO包含的变量值为undefined,如果在此时运行代码,肯定会报错,因此JavaScript引擎无法执行未定义的变量。

在执行阶段,JavaScript引擎会再次读取执行上下文,并用变量的实际值更新VO。编译器再把代码编译为计算机可执行的字节码后执行。如果在代码执行过程中发生异常,JavaScript引擎会抛出异常并停止执行代码。

3.3、销毁阶段

执行上下文销毁阶段是指当一个函数执行完毕或者当前执行上下文被弹出执行上下文栈时,执行上下文会被销毁的过程。在执行上下文销毁阶段,JavaScript引擎会执行以下步骤:

  1. 垃圾回收JavaScript引擎会检查当前执行上下文中的变量对象和函数声明是否被其他对象引用。如果没有被引用,则这些对象将被标记为垃圾对象,并在垃圾回收过程中被清除。
  2. 变量销毁JavaScript引擎会销毁当前执行上下文中的所有变量。在函数执行结束时,所有局部变量将被销毁。在全局执行上下文中,全局变量只有在页面关闭时才会被销毁。
  3. 闭包变量销毁:如果当前执行上下文是一个闭包函数,那么其中的闭包变量将不会被销毁。这是因为闭包变量被外层函数的作用域链所引用,只有当外层函数被销毁时,闭包变量才会被销毁。
  4. 执行上下文弹出JavaScript引擎会将当前执行上下文从执行上下文栈中弹出,并将控制权返回给上一个执行上下文。

🏄🏻 小知识

ES5以上的规范,对于执行上下文的创建过程有所调整,移除了了ES3中的变量对象VO和活动对象AO,引入了词法环境组件LexicalEnvironment component) 和变量环境组件VariableEnvironment component)。

4、执行栈

执行栈又称调用栈,记录了脚本整个生命周期中生成的执行上下文。

🏄🏻 小知识

JavaScrip是单线程语言,也就是说它只能在同一时间执行一项任务。因此,其他的操作、函数和事件发生时,执行上下文也会被创建。由于单线程的特性,一个堆叠了执行上下文的栈就会被创建,称为执行栈

JS引擎会搜索代码中被调用的函数。每一次函数被调用,一个新的FEC就会被创建,并被放置在当前执行上下文的上方。而执行栈最顶部的执行上下文会成为活跃执行上下文,并且始终是JS引擎优先执行。

一旦活跃执行上下文中的代码被执行完毕,JS引擎就会从执行栈中弹出这个执行上下文,紧接着执行下一个执行上下文,以此类推。

4.1、示例代码

用一段代码来描述执行栈的流程

var name = "Guizimo";

function first() {
  var a = "Hi!";
  second();
  console.log(`${a} ${name}`);
}

function second() {
  var b = "Hey!";
  third();
  console.log(`${b} ${name}`);
}

function third() {
  var c = "Hello!";
  console.log(`${c} ${name}`);
}

first();

执行结果:

Hello! Guizimo
Hey! Guizimo
Hi! Guizimo

对于这个预料之中,但总感觉奇奇怪怪的结果...所以还是使用图示来讲解一下。

4.2、图示讲解

  1. JS引擎加载脚本,创建GEC,并压入执行栈的最底部。name变量,firstsecondthird函数在所有函数外部定义,所以位于GEC,并且被VO存储。

    image-20230726172038328

  2. JS引擎遇到first函数调用时,一个新的FEC被创建。新的执行上下文被放置在当前上下文上方,形成执行栈。在first函数调用时,其执行上下文变成活跃执行上下文。在first函数中的变量a ='Hi!'被存储在其FEC中,而非GEC中。

    image-20230726172250272

  1. 紧接着,second函数在first函数中被调用。由于JavaScript单线程的特性,first函数的执行会被暂停,直到second函数执行完闭,才会继续执行。同样的,JS引擎会给second函数设置一个新的FEC,并把它放置在栈顶端,并激活。second函数成为活跃执行上下文,变量b = 'Hey!'被存储在其FEC中。

    image-20230727073234049

  2. 再之后second函数中的third函数被调用,其FEC被创建并放置在执行栈的顶部。

    image-20230727073323502

  3. third函数中的变量c = 'Hello!'被存储在其FEC中,Hello! Guizimo在控制台中打印。等待third函数执行完毕后, 其FEC就从栈顶端弹出,而调用third函数的second函数重新成为活跃执行上下文。

    image-20230727073234049

  4. 回到second函数,控制台打印Hey! Guizimo。函数执行完成所有任务,这个执行上下文从执行栈上弹出。

    image-20230726172250272

  5. first函数执行完毕,从执行栈上弹出后,控制流回到代码的GEC

    image-20230726172038328

  6. 最终,所有代码执行完毕,JS引擎GEC从执行栈上弹出。

博客说明与致谢

文章所涉及的部分资料来自互联网整理,其中包含自己个人的总结和看法,分享的目的在于共建社区和巩固自己。

引用的资料如有侵权,请联系本人删除!

感谢勤劳的自己个人博客GitHub,公众号【归子莫】,小程序【子莫说】

如果你感觉对你有帮助的话,不妨给我点赞鼓励一下,好文记得收藏哟!

幸好我在,感谢你来!


归子莫
1k 声望1.2k 粉丝

信息安全工程师,现职前端工程师的全栈开发,三年全栈经验。