02.JS中的执行上下文(Execution Context)和栈(stack)

139 阅读5分钟

一.变量提升和函数提升

1.1 变量声明提升

** ==通过var定义(声明)的变量, 在定义语句之前就可以访问到==*

** 值: undefined*

 var a = 3

      function fn() {
        console.log(a) //undefined
        var a = 4
      }
      fn()

1.2. 函数声明提升

** ==通过function声明的函数, 在之前就可以直接调用==*

** 值: 函数定义(对象)*

 console.log(b) //undefined  变量提升
      fn2() //可调用  函数提升
      // fn3() //不能  变量提升

      var b = 3

      function fn2() {
        console.log('fn2()')
      }

      var fn3 = function () {
        console.log('fn3()')
      }

问题: 变量提升和函数提升是如何产生的?

二. 执行上下文

简而言之,执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行。

  1. **定义:**执行上下文以一套非常严格的规则,规范变量存储、提升、this指向。
  2. 分类: 全局执行上下文;函数执行上下文
  3. **作用:**对数据进行预处理
  4. ==如何计算执行上下文环境的个数?==
    1. n+1
    2. n:函数调用的次数,函数每调用一次,开启一个对全局数
    3. 1:全局上下文环境

2.1 执行上下文的类型

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

  • 全局执行上下文 — 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:==创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。==
  • 函数执行上下文 — ==每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。==每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。
  • Eval 函数执行上下文 — 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用 eval,所以在这里我不会讨论它。

2.2 全局执行上下文(Execution Context)

  • 全局执行上下文:

    1. 创建一个全局的 window 对象(浏览器的情况下)
    2. 并且设置 this 的值等于这个全局对象。
    3. 一个程序中只会有一个全局执行上下文。
  • ==对全局数据进行预处理==

    • var定义的全局变量==>undefined, 添加为window的属性
    • function声明的全局函数==>赋值(fun), 添加为window的方法
    • this==>赋值(window)
  • 开始执行全局代码

    console.log(a1, window.a1) //undefined undefined,变量提升
    window.a2() //a2(),函数提升
    console.log(this) //window
    
    var a1 = 3
    
    function a2() {
        console.log('a2()')
    }
    console.log(a1) //3
    

2.3 函数执行上下文(Function execution context)

  • 函数执行上下文对象:每当一个函数被调用时, 都会为该函数创建一个新的上下文。
    1. 每当一个函数被调用时, 都会为该函数创建一个新的上下文。
    2. 每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。
    3. 函数上下文可以有任意多个。
    4. 每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。
  • 对局部数据进行预处理
    • 形参变量= =>赋值(实参)= =>添加为执行上下文的属性
    • arguments==>赋值(实参列表), 添加为执行上下文的属性
    • var定义的局部变量==>undefined, 添加为执行上下文的属性
    • function声明的函数 ==>赋值(fun), 添加为执行上下文的方法
    • this==>赋值(调用函数的对象)
  • 开始执行函数体代码

三.执行上下文栈

浏览器中的JS解释器是单线程的。也就是说在浏览器中同一时间只能做一个事情,其他的action和event都会被排队放入到执行栈中(Execution Stack)。下图表示了一个单线程栈的抽象视图

执行上下文栈

关于执行栈,有5点需要记住:

  • 单线程
  • 同步执行
  • 一个全局上下文
  • 无数的函数上下文
  • 每次函数调用都会床架一个新的执行上下文,即使是调用自身

执行上下文栈:

1. 在全局代码执行前, JS引擎就会创建一个栈来存储管理所有的执行上下文对象

2. 在全局执行上下文(window)确定后, 将其添加到栈中(压栈)

3. 在函数执行上下文创建后, 将其添加到栈中(压栈)

4. 在当前函数执行完后,将栈顶的对象移除(出栈)

5. 当所有的代码执行完后, 栈中只剩下window

   <!--
1. 在全局代码执行前, JS引擎就会创建一个栈来存储管理所有的执行上下文对象
2. 在全局执行上下文(window)确定后, 将其添加到栈中(压栈)
3. 在函数执行上下文创建后, 将其添加到栈中(压栈)
4. 在当前函数执行完后,将栈顶的对象移除(出栈)
5. 当所有的代码执行完后, 栈中只剩下window
-->
    <script type="text/javascript">
      var a = 10
      var bar = function (x) {
        var b = 5
        foo(x + b)
      }
      var foo = function (y) {
        var c = 5
        console.log(a + c + y)
      }
      bar(10) //30
      // bar(10)
    </script>

image-20210620163608318

image-20210620163619647

四.面试题

4.1 函数执行上下文栈

问题:1.依次输出什么?2.一共执行了几个上下文栈

console.log('global begin: '+ i) 
var i = 1
foo(1);
function foo(i) {
    if (i == 4) {
        return;
    }
    console.log('foo() begin:' + i);  
    foo(i + 1);                       
    console.log('foo() end:' + i);
}
console.log('global end: ' + i);

// global begin: undefined
// foo() begin:1 
// foo() begin:2
// foo() begin:3
// foo() end:: 3
// foo() end:: 2
// foo() end:: 1
// global end: 1

image-20210620173253233

4.2 测试题1: 先执行变量提升, 再执行函数提升

   /*
   测试题1:  先执行变量提升, 再执行函数提升
   */
      function a() {}
      var a
      console.log(typeof a) // 'function'

4.3 *测试题2:*变量预处理, in操作符

 /*
       测试题2:变量预处理, in操作符
       */
      if (!(b in window)) {
        var b = 1
      }
      console.log(b) // undefined

4.4 测试题3:预处理, 顺序执行

 var c = 1

      function c(c) {
        console.log(c)
        var c = 3
      }
      c(2) // 报错