js 变量提升与函数提升

1. 现象

var str = '123';
console.log(str); // 123
console.log(str); // undefined
var str = '123';
var val = '123';
// 立即执行函数
(function(){
    console.log(val); // 123
})();
var val = '123';
// 立即执行函数
(function(){
    console.log(val); // undefined
    var val = '测试';
})();

2. 预解析

2.1 核心:预解析

当看到 var a = 2; 时,可能会认为这是一个声明。但 JavaScript 实际上会将其看成两个声明 var a;a = 2;

  • 第一个定义声明是在编译阶段进行的;
  • 第二个赋值声明会被留在原地等待执行阶段;

即代码是这样写的:

// 我们看到的代码:
var name = '123';

但Js会将它解析成:

// 声明(Declaration)
var name; // 声明但未初始化,所以分配 undefined

// 初始化(Initialization)
name = '123'; // 初始化(赋值)

2.2 只有声明被提升了

只有声明会被提升,而赋值和其他代码逻辑会在执行到代码的位置时才会生效。所以才会有下面的问题:

foo();
function foo() {
    console.log(name); // undefined
    var name = '123';
}

注:函数被提升了,自然可以正常执行,但变量仅仅是声明被提升了。

2.3 每个作用域都会进行提升操作

上面的代码实际在编译时是这样的:

function foo(){
    var name; // 声明
    console.log(name); // undefined
    name = '123'; // 初始化
}
foo(); // 函数执行

3. 提升之间的优先级

3.1 函数会首先被提升,然后才是变量

3.2 函数字面量不会进行函数提升

最直观的例子,就是在函数字面量前调用该函数:

foo();

var foo = function(){
    console.log(1);
}
// TypeError: foo is not a function

在上面这段程序中:

  • 变量标识符 foo 被提升并分配给所在作用域(在这里是全局作用域),因此在执行 foo() 时不会导致ReferenceError(), 而是会提示 foo is not a function
  • 然后就是执行 foo,foo 此时并没有赋值(注意变量被提升了)。由于对 undefined 值进行函数调用而导致非法操作,因此抛出 TypeError 异常。

4. ES6和小结

ES6 新增了两个命令 let 和 const,用来声明变量,变量声明在它们身上不会存在。

4.1 变量提升是可以规避的

let 命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。

// var 的情况
console.log(foo); // 输出undefined
var foo = 2;

// let 的情况
console.log(bar); // 报错ReferenceError:Cannot access 'bar' before initialization
let bar = 2;

变量 bar 用 let 命令声明,不会发生变量提升。这表明在声明它之前,变量 bar 是不存在的,这时如果用到它,就会抛出一个错误。

4.2 小结

  • 变量提升:函数声明和变量声明总是会被解释器悄悄地被“提升”到方法的最顶部,但变量的初始化不会提升;
  • 函数提升:函数声明可以被看做是函数的整体被提升到了代码的顶部,但函数字面量表达式并不会引发函数提升;
  • 函数提升优先于变量提升;
  • le t和 const 可以有效的规避变量提升;