什么是变量提升?
我们都知道,js执行都是自上往下执行的,但是函数及变量的声明都将被提升到函数或全局的最顶部,也就是说,我们可以先使用变量再声明变量,下面是一个栗子。
console.log(a)
console.log(b)
b()
var a = 1
function b () {
console.log('I am b')
}
这时候使用console.log()输出a和b以及调用b方法都不会报错,而是会打印 undefined、function b () {...}、I am b。

Why?
输出上图的原因是因为js的变量提升在“搞鬼”,当使用var或函数声明来定义一个变量的时候,js会偷偷的将这些变量的定义提升到函数或全局的最顶部,同时var声明的变量会赋初始值为 undefined,函数声明则会将整个函数提升到顶部,上面的栗子可以理解成如下执行流程:
var a = undefined
function b () {
console.log('I am b')
}
console.log(a) // undefined
console.log(b) // f b() {...}
b() // I am b
...
1. 变量提升优先级
当同时遇到命名相同的变量声明和函数声明的时候又会怎么样?看下面这个例子:
console.log(name)
var name = 1
function name () {}
上面的console.log()将会打印 ƒ name () {}, 这就说明了:
在命名相同的情况下,函数声明的优先级大于变量声明的优先级。
2. 函数整体提升及同名优先级
直接看下面的栗子
console.log(a)
var a = 1
console.log(a)
function a () { console.log(1) }
console.log(a)
function a () { console.log(2) }
console.log(a)
a = 2
console.log(a)
分析
- 同时遇到了使用
var和function定义的a,由于变量提升优先级函数大于变量的原因,会优先将函数声明的提升到最顶部。 - 在同名的函数声明中,后面声明的函数会覆盖前面声明的函数。
所以这段代码的执行流程和打印内容为:
// 打印内容
// ƒ a () { console.log(2) }
// 1
// 1
// 1
// 2
// 执行流程
function a () { console.log(2) }
console.log(a) // ƒ a () { console.log(2) }
a = 1
console.log(a) // 1
console.log(a) // 1
console.log(a) // 1
a = 2
console.log(2) // 2
3. 函数声明&函数表达式
函数声明:以
function开头并确定函数名定义函数。
function name ([param...]) { ... }
function myFun () {
return 1
}
函数表达式:将函数赋值给一个变量,函数名可以省略。
var variable_name = function [name] ([param...]) { ... }
// 不带函数名
var fun1 = function () {
return 1
}
// 带函数名
var fun2 = function fun () {
return 2
}
// IIFE
(function fun3 () {
console.log(3)
})()
看下面的这段代码:
console.log(a)
a()
var a = function () { return 1 }
要区分好函数声明和函数表达式的区别,上述代码中,先打印a,然后调用a(),在声明a的函数表达式,但函数表达式不会将函数一起提升到最顶部,而是像简单的 var b = 1 那样把 a 提升到顶部并赋值为 undefined(如下),所以控制台将会打印 undefined 和抛出 TypeError: a is not a function 错误。
var a = undefined
console.log(a) // undefined
a() // [TypeErroe: a is not a function]
a = function () { return 1 }

4. ES6中的变量提升
ES6中新增了两个重要的关键字:let 和 const,那么使用这两个关键字声明的变量提升又会怎么样呢?看下面的栗子:
console.log(a)
a = 2
let a = 1
console.log(a)
分析

上述代码会抛出 [ReferenceError: Cannot access 'a' before initialization],这里报错的原因是ES6规定:
区块里出现
let和const声明的变量或常量,则这个区块的起始位置到声明位置会形成一个封闭区,在该封闭区内禁止使用这些变量,一旦使用就会报ReferenceError错误,这个封闭区在语法上称为 暂时性死区(temporal dead zone,简称 TDZ)。
// TDZ开始
console.log(a) // ReferenceError
a = 2 // ReferenceError
let a = 1 // TDZ结束
console.log(a) // 1
let/const还会变量提升吗?
对于ES6中 let 和 const 会不会变量提升的说法有很多种,但小编的看法是 会 变量提升的 (仅个人看法)。let 声明的变量无法在声明前使用,但这不代表不会提升,只是因为ES6中 TDZ 的原因使得这个变量在声明前不给使用而已,ES6中引入了块级作用域的概念:
当块级作用域下出现
let,const声明的变量,该变量会绑定到当前作用域中,作用域外部将无法访问该变量。
代码1
{
// 作用域1
let a = 1
{
// 作用域2
a = 2
console.log(a) // 2
}
console.log(a) // 2
}
上面的代码中,作用域1有一个使用 let 声明的变量 a,赋值1,作用域2在作用域1内部,可以访问作用域1中的 a 并将作用域1中的 a 改为2,使用 console.log() 打印a时,a 可以正常打印并打印2。

代码2
{
// 作用域1
let a = 1
{
// 作用域2
a = 2 // ReferenceError
let a
console.log(a)
}
console.log(a)
}
代码2和代码1的区别在于作用域2中,在 a 赋值为2下面使用 let 声明了变量 a,如果用 临时性死区 来解释作用域2的话将会是如下所示:
{
// 作用域2
// TDZ开始
a = 2 // ReferenceError
let a // TDZ结束
console.log(a) // undefined
}
如果变量不提升,那么 a = 2 则会将作用域1的 a 从 1 改为 2,但运行代码2时会抛出 ReferenceError: Cannot access 'a' before initialization的错误。

总结一下
- 使用var和函数声明时,会将变量提升到函数或全局顶部,
var声明会赋值undefined,函数声明会将整个函数提升到顶部,即可以先使用后声明。 - 函数声明的提升优先级大于变量提升的优先级。
- 声明两个同名函数时,后面声明的函数会覆盖前面声明的函数。
- 函数表达式不会将函数提升到顶部。
- 使用
let和const声明变量(常量)时,作用域顶部到声明位置会出现 暂时性死区,该区域内使用变量会报ReferenceError错误。 let和const定义的变量会变量提升(仅个人看法)。- 在实际项目开发过程中,变量提升似乎也派不上用场,但也尽量不要使用变量提升这一特性,养成代码规范,先声明再使用或者使用JavaScript的 严格模式(strict mode):
"use strict",还有就是使用 ESLink 规范项目代码。
