JS执行上下文

194 阅读7分钟

什么是执行上下文?

js在执行语句前,经过了一系列的“准备”,为代码执行创造了一个“教室“————执行上下文

执行上下文是 JavaScript 代码执行时的环境。你可以把它想象成一个“教室”,在这个教室里,JavaScript 引擎为代码执行做好了所有准备工作。执行上下文包含了代码执行所需的所有信息,比如变量、函数、作用域链等。

屏幕截图 2025-02-03 162023.png

js在哪里查找执行上下文呢?

JavaScript 通过 执行栈(Call Stack)来管理执行上下文。执行栈是一个先进后出的数据结构,当前执行上下文总是位于栈顶。

  • 执行栈栈顶的执行上下文称为当前执行上下文

  • js代码总是在当前执行上下文中运行

    • 意思是js代码运行时所用到的资源总是在当前执行上下文中查找

屏幕截图 2025-02-03 162826.png

什么情况下会创建新的执行上下文?

在 JavaScript 中,有四种情况会创建新的执行上下文:

  1. 全局代码执行:创建全局执行上下文。
  2. 函数调用:创建函数执行上下文。
  3. eval 函数调用:创建 eval 执行上下文(不推荐使用)。
  4. 模块代码执行:创建模块执行上下文。

下面我们重点分析全局执行上下文和函数执行上下文的创建过程。

全局执行上下文

屏幕截图 2025-02-03 164827.png

执行步骤

步骤一:

  • 创建全局执行上下文,压入执行栈

    全局执行上下文的文本环境由两部分组成: 全局对象(window)和 全局scope (script)

步骤二:分析

  • 找到所有非函数中的var声明
  • 找到顶级的函数声明
  • 找到顶级的let,const,class声明
  • 找到块级中的函数声明({}),函数名不与上述的变量名字重复

步骤三:

  • 变量名重复处理

    1.let const class 声明的名字不能重复,自身重复也不行

    2.let const class 和var function的名字不能重复

    3.var和function名字重复,function声明的函数优先

步骤四:创建绑定

  • 登记并初始化var为undefined
  • 顶级函数声明:登记functiion名字,并初始化为新创建的函数对象
  • 块级中的函数声明:登记名字,初始化为undefined
  • 登记let,const,class,但未初始化

1.var和function声明创建在全局对象中,而let,const,class声明的变量在全局scope中

2.先到全局scope中查找,查不到再去全局对象中查找

步骤四:

  • 执行语句

进入function函数体代码

  • 绑定函数对象时,函数对象体内会保存函数创建时的执行上下文的文本
  • 函数执行上下文的文本环境只有一个函数scope,与全局执行上下文不同
  • 函数执行上下文的文本环境会链接到体内保存的文本环境

屏幕截图 2025-02-03 173426.png 所以这段代码结果会报错,因为代码运行时,在当前执行上下文中查找变量a,找到了a,但a没有初始化

从这里可以看出,其实let,const声明的变量都有提升,只是没有初始化,不能使用,也被称为暂时性死区(Temporal dead zone)

这里聊一下作用域的概念

  • 作用域就是解析(查找)变量名的一个集合,就是当前执行上下文(也可以是当前上下文的词法环境(Lexical Environment))

    • 全局作用域就是全局执行上下文
    • 函数作用域就是函数执行上下文
  • 变量不会单独存在,属于一个作用域(编译阶段)

    • 作用域相当于变量的查找规则,首先在当前作用域查找,如果没有找到,就向上一级作用域查找,一直冒泡,直到全局作用域,还没找到——未定义(执行阶段)

另外还有十分重要的点:

  • 函数调用时的执行上下文看”身世“—函数在哪里创建,就保存那里的运行上下文
  • 函数的作用域是在函数创建的时候决定的,而不是调用的时候决定的

屏幕截图 2025-02-03 180624.png

结果输出 2

  • 并非根据调用嵌套(运行上下文)形成作用域链,而是根据函数创建嵌套形成作用域链,也就是函数的书写位置,因此称为词法作用域

屏幕截图 2025-02-05 112428.png

执行代码块时

不会创建新的执行上下文,只会创建新的记录环境

步骤一:

  • 创建新的记录环境,链接在原来记录之前

步骤二:

  • 找到块中let,const声明
  • 块中所有的顶级函数声明

步骤三:

  • 检查名字重复

步骤四:创建绑定

  • 在新的记录环境中绑定

步骤五:

  • 执行语句

屏幕截图 2025-02-03 182327.png

屏幕截图 2025-02-05 111418.png 块内语句执行完之后,js引擎会销毁新创建的记录环境,并链接回原来的文本环境

屏幕截图 2025-02-05 111438.png

所以第一次打印 in if statement ,第二次打印out if statement

在之前提到的全局执行上下文中,记录块中的函数声明有些细节

顶级函数声明和块中的函数声明在记录到文本环境时的差异:

1.顶级函数声明会被初始化为函数对象

2.块中的函数声明若与之前记录的变量名存在重复,就对块中的函数声明不做任何处理

3.块中声明的函数声明没有重名,在全局对象中创建一个以函数名为名的变量,并初始化为undefined

屏幕截图 2025-02-03 210813.png

屏幕截图 2025-02-05 111532.png 在运行代码块时,如之前所说会创建新的记录环境,并且进行变量记录:

屏幕截图 2025-02-03 211902.png

在代码块运行结束之后,与之前不一样的是,除了链接回原来的文本环境,if块作用域不会被销毁,因为foo函数内部保存了对这块的引用。除此之外,在执行到foo函数声明时还会将块作用域中foo函数对象赋值给全局对象中的同名变量,如果不存在同名,则不做操作。

在运行foo函数时,创建foo函数执行上下文:

屏幕截图 2025-02-03 213113.png

总结一下就是

代码块中的函数声明也有提升,并且初始值为undefined,它真正被实例化为函数对象是在执行代码块时,创建了新的块级作用域中完成的。

小练习:

 ​
         console.log(foo)//undefined 并不会报错
         if(false) {
             function foo() {}
         }

2.

         console.log(foo) //undefined
         if(false) {
             function foo() {
                 console.log("foo function")
             }
         }
         foo() // Uncaught TypeError: foo is not a function

因为判断为假,不会执行if代码块,而foo函数对象的创建又是执行代码块时完成的 3.

         console.log(foo) //undefined
         if(true) {
             function foo() {
                 console.log("foo function")
             }
         }
         foo() // foo function

4.

         var foo 
         if(true) {
             function foo() {
                 console.log("foo function")
             }
         }
         foo() //foo function

5.

         let foo 
         if(true) {
             function foo() {
                 console.log("foo function")
             }
         }
         foo() // Uncaught TypeError: foo is not a function

循环中的执行上下文

      var liList  =[]
      for(var i=0;i<5;i++){
         liList[i]  = function(){
             console.log(i)
         }
      }
      liList[0]()
      liList[1]()
      liList[2]()
      liList[3]()
      liList[4]()

结果是输出五个5

第一次进入循环时

屏幕截图 2025-02-05 111223.png

然后给liList[0]赋值,这里不是函数声明,而是赋值操作,所以不会将此函数对象保存在块级作用域中。同时,之前反复强调的一点,函数创建时会保存当前执行上下文的文本环境。

屏幕截图 2025-02-05 111834.png 当循环结束之后:

屏幕截图 2025-02-05 112752.png

每个函数执行时,都是到全局对象中找到的i,因此全部输出5

      var liList  =[]
      for(let i=0;i<5;i++){
         liList[i]  = function(){
             console.log(i)
         }
      }
      liList[0]()
      liList[1]()
      liList[2]()
      liList[3]()
      liList[4]()

结果正常输出0,1,2,3,4

如果是用let声明的i,在执行到for()时,会创建一个新的记录环境。

屏幕截图 2025-02-05 114151.png

之后每次i的迭代都会创建一个副本

屏幕截图 2025-02-05 120443.png

函数执行时:

屏幕截图 2025-02-05 120732.png

总结下来就是for() 内定义的循环变量的作用域由关键字决定:var 会在函数或全局范围内,letconst 只在循环内有效。