JavaScript 的提升

最近在回顾 JavaScript 的基础。

于是碰到了 提升 这个概念

关于,提升,一直有印象的,就是 var 这个变量提升。

提示:
每个作用域都会有“提升”操作,提升操作会将变量提升到作用域的顶部。

var 的提升

使用 var 声明的变量,会在所有的作用域中提升。

  1. 基本

    1
    2
    var a = 'a001'
    console.log(a) // a001
  2. 先打印,接着声明,赋值

    1
    2
    console.log(b) // undefined. ReferenceError: b is not defined
    var b = 'b001'

    在编译器中等价于

    1
    2
    3
    var b
    console.log(b) // undefined. ReferenceError: b is not defined
    b = 'b001'
  3. 重复声明并赋值

    1
    2
    3
    var c = 'c001'
    var c = 'c002'
    console.log(c) // c002
    • 但是你假如对使用 let/const 声明的变量进行了重复声明,那么就会报错。

      1
      2
      3
      let c1 = 'c1001'
      let c1 = 'c1002' // SyntaxError: Identifier 'c1' has already been declared
      console.log(c1)
      1
      2
      3
      const c2 = 'c2001'
      const c2 = 'c2002' // SyntaxError: Identifier 'c2' has already been declared
      console.log(c2)
  4. 先“赋值”,再声明

    1
    2
    3
    d = 'd001'
    var d
    console.log(d) // d001
  5. 仅“赋值”,就会变成 window 上的变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    e = 'e001'
    console.log(e) // e001。若后续同一作用域下代码无 `var e`,那这 `e` 便是 window.e 了。

    // or
    function f() {
    e = 'e002'
    }
    f()
    console.log(e) // e002。 跟上面的 `e`,是相同的情况

    // but
    if (false) {
    e = 'e003'
    }
    console.log(e) // undefined。 跟上面的两个 `e`,是不同的情况。因为不存在变量提升,代码也没有执行 if 块里的
  6. 在函数中

    1
    2
    3
    4
    5
    6
    function g() {
    var h = 'h001'
    console.log('in function', h) // h001
    }
    g()
    console.log('in window', h) // undefined。因为函数内部的变量,不会提升到作用域的顶部,因为该变量会在调用函数之后随即被销毁掉。

函数的提升

或许是好久没看概念,这两天看书发现:

函数里也是有“提升”的。

但仅限于函数声明会有提升,而函数表达式是不会提升的。

函数声明会在任何代码执行之前先被读取并添加到执行上下文。这个过程叫作函数声明提升( function declaration hoisting)。在执行代码时, JavaScript 引擎会先执行一遍扫描,把发现的函数声明提升到源代码树的顶部。因此即使函数定义出现在调用它们的代码之后,引擎也会把函数声明提升到顶部。

– 《JavaScript 高级程序设计(第 4 版)》P297

具体表现为

1
2
3
4
5
// 没问题
console.log(sum(10, 10)) // 20
function sum(num1, num2) {
return num1 + num2
}

但是把上述的函数声明改为等价的函数表达式,那么执行的时候就会出错。

1
2
3
4
console.log(sum(10, 10)) // Uncaught ReferenceError: sum is not defined
let sum = function (num1, num2) {
return num1 + num2
}

哪怕是你换成用 var 声明的函数,也会出错。

1
2
3
4
console.log(sum(10, 10)) // Uncaught ReferenceError: sum is not defined
var sum = function (num1, num2) {
return num1 + num2
}

除了“函数什么时候真正有定义”这个区别之外,这两种写法是等价的。

提升的优先级

重复的声明,将被忽略掉

先看点简单的例子

1
2
3
4
5
6
7
8
foo()
var foo
function foo() {
console.log(1)
}
foo = function () {
console.log(2)
}

上述代码执行的结果是:1

在编译器中,被理解为:

1
2
3
4
5
6
7
function foo() {
console.log(1)
}
foo()
foo = function () {
console.log(2)
}

注:foo() 这里的 foo 只是一个函数名,不是函数声明。函数声明是包含函数名的。在此也可以理解为是个变量,函数变量。跟之后的 var foo 中的 foo 是相同的,同名变量。所以,后面的 var foo 将被忽略掉。因为是重复的声明

函数声明会被提升到变量声明之前

再来看看这个例子

1
2
3
4
5
6
7
8
9
10
11
console.log('a -1', a) // a -1 f a()
console.log('a() -2', a()) // 这里是先执行 a(),后打印返回值
var a = 1
function a() {
console.log('函数 a', 1)
return `函数 a 2`
}
console.log('a -8', a) // a -8 1
a = 3
console.log('a() -10', a()) // a not a function,该错误会阻断后续的代码执行
console.log('a -11', a)

重复的声明,将被忽略掉

变量的值,只会在重新赋值后才会被改写。而重复的声明,会被忽略,并不会影响什么。

援引自 tc39/ecma262 中的阐述:14.3.2 Variable Statement

A variable defined by a VariableDeclaration with an Initializer is assigned the value of its Initializer’s AssignmentExpression when the VariableDeclaration is executed, not when the variable is created.

  1. 变量的第一次声明,会被默认赋值为 undefined。假如接着赋值,那么就会覆盖掉默认值。

    • var,let 只声明,不赋值,默认值为 undefined

      • 除非是显式的赋值,否则变量的值,在非第一次执行变量声明时,是不会被赋值的。是无效的,会被编译器忽略掉。
    • const 必须声明并赋值(初始化),否则报错。

      • 只声明,不赋值。是无效的声明。

      • 只声明,或再打印它: const 声明中缺少初始化器。

        1
        2
        3
        4
        const b // SyntaxError: Missing initializer in const declaration
        // or
        const b
        console.log(b) // SyntaxError: Missing initializer in const declaration
      • 当使用这个变量去赋值给其他变量的时候,会提示未定义

        1
        2
        const b
        const c = b // ReferenceError: b is not defined

在 JavaScript 引擎里,

1
var a = 2

被看作两个单独的语句

1
2
var a // 编译阶段的任务
a = 2 // 执行阶段的任务

这意味着无论作用域中的声明出现在什么地方, 都将在代码本身被执行前首先进行处理。可以将这个过程形象地想象成所有的声明((var)变量和函数(声明)) 会被“移动” 到各自作用域的最顶端, 这个过程被称为提升。

声明本身会被提升, 而包括函数表达式的赋值在内的赋值操作并不会提升。

要注意避免重复声明, 特别是当普通的 var 声明和函数声明混合在一起的时候, 否则会引起很多危险的问题!

《你不知道的 JavaScript(上卷)》 4.4 小结