变量声明中的变量提升(var hoisting)

今天讲讲变量声明和变量提升(var hoisting)。有一类题目会问你在变量声明前去获取这个变量值,会获取到什么值或者产生什么情况。我以下面的示例代码来说明下,当我们运行一个函数时发生了什么?(为了言简意赅,我简化了规范中的步骤,只做一个大概的说明,详情请参考 ES6 规范)

var foo1 = 'foo1';
var foo2 = 'foo2Outer';

function exampleFunc() {
  console.log( foo1 ); // foo1
  console.log( foo2 ); // undefined
  console.log( bar ); // undefined
  console.log( varFunc ); // undefined
  console.log( expFunc ); // function expFunc() {alert( 'second' )}
  console.log( localVarUndefined );
  // Firefox -> ReferenceError: can't access lexical declaration `localVarUndefined' before initialization
  // Chrome -> Uncaught ReferenceError: localVarUndefined is not defined
  console.log( localVar ); 
  // Firefox -> ReferenceError: can't access lexical declaration `localVar' before initialization
  // Chrome -> Uncaught ReferenceError: localVar is not defined

  var foo2 = 'foo2Inner'
  var bar = 'bar';
  var varFunc = function() {
  }
  function expFunc() {
    alert( 'first' );
  }
  function expFunc() {
    alert( 'second' );
  };
  var expFunc;
  /* ----- let declared variable ---- */
  let localVarUndefined;
  let localVar = 'localVar';

  console.log( localVarUndefined ); // undefined
  console.log( localVar ); // localVar
}

exampleFunc();

exampleFunc 函数被调用后发生的步骤:

  1. 生成执行上下文和作用域并做一定的初始化。(如果对执行上下文是什么不了解的话,详见《什么是作用域和执行上下文》。初始化过程还涉及 this 的初始化,详见《function作为构造函数和非构造函数调用的区别》)
  2. 对形式参数进行初始化(这篇我们就先不具体解释这步)。
  3. 遍历函数体,寻找声明的变量,并按一定的规则生成这些变量放在作用域中(注意这里仅仅是生成变量,忽略等号后面赋值语句,即使声明和赋值写在一起)。
  4. 按程序逻辑从上到下运行函数体。

接下来我们逐一分析示例代码中的各种情况:

  • foo1
    变量 foo1 在函数中并没有声明,所以这里只涉及变量寻值的问题,所以取值就是全局变量 foo1。

  • foo2,bar
    这两个变量按上面函数被调用的步骤所示,在运行函数体之前两个变量已经生成,去操作并不会产生运行时错误。又由于运行 console 的时候还没有运行到下方 var xxx = yyy 的赋值语句,所以此时打印出来的是 undefined。(对!你没有想错,当运行函数体时,var xxx = yyy 其实已经变成单纯的赋值语句了。)

  • varFunc,expFunc
    varFunc 虽然是一个函数,但是是用 var 关键字声明的,其实可以和 foo2,bar 归为一类,所以打印出来也是 undefined。expFunc 既有作为函数声明,又有用 var 声明,这里就涉及到上述步骤3中的子步骤了:

    1. 先搜寻函数声明,并以函数名创建变量,如果有多个同名函数声明,取最后一个为准。
    2. 再搜寻 var 声明,如果查询到 var 声明的变量已经在上一步函数证明过程中占用了,则不做任何操作。如果没有占用再创建一个变量。
    3. 对第一步中的函数(名)变量进行函数初始化。

所以 expFunc 在函数体运行前,变量创建过程中变成了 function expFunc() {alert( 'second' )}。当然如果函数体运行后再对 expFunc 进行赋值后,其又可以变成其他:

function exampleFunc() {
  console.log( expFunc ); // function expFunc() {alert( 'second' )}
  
  function expFunc() {
    alert( 'first' );
  }
  function expFunc() {
    alert( 'second' );
  };
  console.log( expFunc ); // function expFunc() {alert( 'second' )}
  var expFunc = 'str';
  console.log( expFunc ); // str
}

exampleFunc();



到这里为止,我们看到了变量声明在 ES6 之前的大致行为,这就是所谓的变量提升(var hoisting):函数运行的过程像是把函数体内所有变量的声明(创建)都提到了函数的最前面。

  • localVarUndefined,localVar
    但是 ES6 中加入了 let 局部变量的声明,它的行为就和 var 声明不同了,那我们来看下区别在哪里?其实 exampleFunc 函数被调用后发生的步骤一点都没有变化,依然会在函数体运行前进行变量的搜寻和创建。但是当遇到 let 声明的变量,规范规定在这个变量创建后是无法获取到的,直到 LexicalBinding 阶段。那什么是 LexicalBinding 阶段呢?就是函数体运行到 let xxx [= yyy]; 这条语句时。换句话说就是当函数开始执行,然后逐行运行到 let 声明(或者外加赋值)语句前,let 声明的变量虽然被创建了,但是程序获取不到,所以就会抛出 ReferenceError。这段无法获取变量的阶段称之为 TDZ(Temporal Dead Zone)。如此就能解释 localVarUndefined,localVar 的打印结果了。

你可能感兴趣的:(变量声明中的变量提升(var hoisting))