变量声明中的变量提升(var hoisting)
2017-05-10 本文已影响302人
唯泥Bernie
今天讲讲变量声明和变量提升(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 函数被调用后发生的步骤:
- 生成执行上下文和作用域并做一定的初始化。(如果对执行上下文是什么不了解的话,详见《什么是作用域和执行上下文》。初始化过程还涉及 this 的初始化,详见《function作为构造函数和非构造函数调用的区别》)
- 对形式参数进行初始化(这篇我们就先不具体解释这步)。
- 遍历函数体,寻找声明的变量,并按一定的规则生成这些变量放在作用域中(注意这里仅仅是生成变量,忽略等号后面赋值语句,即使声明和赋值写在一起)。
- 按程序逻辑从上到下运行函数体。
接下来我们逐一分析示例代码中的各种情况:
-
foo1
变量 foo1 在函数中并没有声明,所以这里只涉及变量寻值的问题,所以取值就是全局变量 foo1。 -
foo2,bar
这两个变量按上面函数被调用的步骤所示,在运行函数体之前两个变量已经生成,去操作并不会产生运行时错误。又由于运行 console 的时候还没有运行到下方 var xxx = yyy 的赋值语句,所以此时打印出来的是 undefined。(对!你没有想错,当运行函数体时,var xxx = yyy 其实已经变成单纯的赋值语句了。) -
varFunc,expFunc
varFunc 虽然是一个函数,但是是用 var 关键字声明的,其实可以和 foo2,bar 归为一类,所以打印出来也是 undefined。expFunc 既有作为函数声明,又有用 var 声明,这里就涉及到上述步骤3中的子步骤了:- 先搜寻函数声明,并以函数名创建变量,如果有多个同名函数声明,取最后一个为准。
- 再搜寻 var 声明,如果查询到 var 声明的变量已经在上一步函数证明过程中占用了,则不做任何操作。如果没有占用再创建一个变量。
- 对第一步中的函数(名)变量进行函数初始化。
所以 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();
<br />
到这里为止,我们看到了变量声明在 ES6 之前的大致行为,这就是所谓的变量提升(var hoisting):函数运行的过程像是把函数体内所有变量的声明(创建)都提到了函数的最前面。
- localVarUndefined,localVar
但是 ES6 中加入了 let 局部变量的声明,它的行为就和 var 声明不同了,那我们来看下区别在哪里?其实 exampleFunc 函数被调用后发生的步骤一点都没有变化,依然会在函数体运行前进行变量的搜寻和创建。但是当遇到 let 声明的变量,规范规定在这个变量创建后是无法获取到的,直到 LexicalBinding 阶段。那什么是 LexicalBinding 阶段呢?就是函数体运行到 let xxx [= yyy]; 这条语句时。换句话说就是当函数开始执行,然后逐行运行到 let 声明(或者外加赋值)语句前,let 声明的变量虽然被创建了,但是程序获取不到,所以就会抛出 ReferenceError。这段无法获取变量的阶段称之为 TDZ(Temporal Dead Zone)。如此就能解释 localVarUndefined,localVar 的打印结果了。