前端基本功(六):javascript为什么会存在变量提升(执行上下文、执行上下文堆栈)

222 阅读4分钟

1. 什么是执行上下文

当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。

2. 可执行代码(EC)的类型

  1. 全局代码
  2. 函数代码
  3. Eval代码

3. 执行上下文堆栈

  1. 浏览器里的JavaScript解释器被实现为单线程。这意味着同一时间只能发生一件事情,其他的行文或事件将会被放在叫做执行栈里面排队。JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文。
  2. 当 JavaScript 开始要解释执行代码的时候,最先遇到的就是全局代码,所以初始化的时候首先就会向执行上下文栈压入一个全局执行上下文,我们用 globalContext 表示它,并且只有当整个应用程序结束的时候,ECStack 才会被清空,所以程序结束之前, ECStack 最底部永远有个 globalContext。
  3. 在你的全局代码中调用一个函数,你程序的时序将进入被调用的函数,并创建一个新的执行上下文,并将新创建的上下文压入执行栈的顶部,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。

4. 调用执行上下文,分为两个阶段

  1. 创建阶段【当函数被调用,但未执行任何其内部代码之前】:
    • 创建作用域链(Scope Chain
    • 创建变量,函数和参数(该过程是有先后顺序的:函数的形参==>>函数声明==>>变量声明
    • 求”this“的值
  2. 激活/代码执行阶段:
    • 初始化变量的值和函数的引用,解释/执行代码。

5. 解释器执行代码的伪逻辑

  1. 查找调用函数的代码。
  2. 执行代码之前,先进入创建上下文阶段:
    • 初始化作用域链
    • 创建变量对象:
      • 创建arguments对象,检查上下文,初始化参数名称和值并创建引用的复制。
      • 扫描上下文的函数声明(而非函数表达式):为发现的每一个函数,在变量对象上创建一个属性——确切的说是函数的名字——其有一个指向函数在内存中的引用。如果函数的名字已经存在,引用指针将被重写。
      • 扫描上下文的变量声明:为发现的每个变量声明,在变量对象上创建一个属性——就是变量的名字,并且将变量的值初始化为undefined。如果变量的名字已经在变量对象里存在,将不会进行任何操作并继续扫描。
    • 求出上下文内部“this”的值。
  3. 激活/代码执行阶段:在当前上下文上运行/解释函数代码,并随着代码一行行执行指派变量的值。

5. 提升(Hoisting)

(function() {
    console.log(typeof foo); // 函数指针
    console.log(typeof bar); // undefined

    var foo = 'hello',
        bar = function() {
            return 'world';
        };
        
    function foo() {
        return 'hello';
    }
}());
  1. 为什么我们能在foo声明之前访问它?

    回想在执行代码之前,先进入创建阶段,我们知道函数在该阶段就已经被创建在变量对象中。所以在函数开始执行之前,foo已经被定义了。

  2. Foo被声明了两次,为什么foo显示为函数而不是undefined或字符串?

    我们知道,在创建阶段,函数声明是优先于变量被创建的。而且在变量的创建过程中,如果发现变量对象VO中已经存在相同名称的属性,则不会影响已经存在的属性。因此,对foo()函数的引用首先被创建在活动对象里,并且当我们解释到var foo时,我们看见foo属性名已经存在,所以代码什么都不做并继续执行。

  3. 为什么bar的值是undefined?

    bar采用的是函数表达式的方式来定义的,所以bar实际上是一个变量,但变量的值是函数,并且我们知道变量在创建阶段被创建但他们被初始化为undefined,这也是为什么函数表达式不会被提升的原因。

6. 总结

  1. 执行上下文(EC)分为两个阶段,创建执行上下文和执行代码。
  2. EC创建的过程是由先后顺序的:参数声明 > 函数声明> 变量声明。