《JavaScript的那些事》之变量提升

497 阅读5分钟

什么是变量提升?

我们都知道,js执行都是自上往下执行的,但是函数及变量的声明都将被提升到函数或全局的最顶部,也就是说,我们可以先使用变量再声明变量,下面是一个栗子。

console.log(a)
console.log(b)
b()

var a = 1
function b () {
    console.log('I am b')
}

这时候使用console.log()输出a和b以及调用b方法都不会报错,而是会打印 undefinedfunction 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)

分析

  1. 同时遇到了使用 varfunction 定义的a,由于变量提升优先级函数大于变量的原因,会优先将函数声明的提升到最顶部。
  2. 在同名的函数声明中,后面声明的函数会覆盖前面声明的函数。

所以这段代码的执行流程和打印内容为:

// 打印内容
// ƒ 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中新增了两个重要的关键字:letconst,那么使用这两个关键字声明的变量提升又会怎么样呢?看下面的栗子:

console.log(a)
a = 2
let a = 1
console.log(a)

分析

上述代码会抛出 [ReferenceError: Cannot access 'a' before initialization],这里报错的原因是ES6规定:

区块里出现 letconst 声明的变量或常量,则这个区块的起始位置到声明位置会形成一个封闭区,在该封闭区内禁止使用这些变量,一旦使用就会报 ReferenceError 错误,这个封闭区在语法上称为 暂时性死区(temporal dead zone,简称 TDZ)

// TDZ开始
console.log(a) // ReferenceError
a = 2 // ReferenceError
let a = 1 // TDZ结束
console.log(a) // 1

let/const还会变量提升吗?

对于ES6中 letconst 会不会变量提升的说法有很多种,但小编的看法是 变量提升的 (仅个人看法)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的错误。


总结一下

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