JS基础(三)提升

欢迎阅读,本篇主要介绍JS中的提升.

前言

到目前为止,我们已经很熟悉作用域的概念了,函数作用域和块作用域的概念是一样的:任何声明在某个作用域内的变量,都将附属于这个作用域。我们直觉上会认为JS代码在执行时是自上而下一行一行执行的,但实际上,这并不完全正确,请继续阅读。

什么是提升?

思考一个问题:是声明在前还是赋值在前?
在讨论这个问题前,我们需要了解一点编译原理的知识。在传统编译语言的流程中,程序中的一段源码在执行前会经历三个步骤,统称为 编译

  1. 分词 / 词法分析
    这个过程会将由字符组成的字符串分解成对编程语言来说有意义的代码块,这些代码块被统称为 词法单元。例如:var a = 2;这段程序会被分解为词法单元:var、a、=、2、;。空格是否会被当作词法单元,取决于空格在这门语言中是否有意义。
  2. 解析 / 语法分析
    这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树(AST),被称为 抽象语法树
  3. 代码生成
    将AST转换为可执行代码的过程被称为代码生成,这个过程与语言、目标平台等息息相关。简单来说就是有某种方法可将 var a = 2; 的AST转化为一组机器指令,用来创建一个叫作 a 的变量(包括分配内存等),并将一个值存储在 a 中。

比起那些编译只需要 3 个步骤的语言的编译器,js引擎要复杂的多,例如:在词法分析和代码生成阶段有特定的步骤来对运行性能进行优化,包括对冗余元素进行优化等。JS引擎不会有大量的时间用来进行优化,因此与其他语言不同,JS的编译过程不是发生在构建之前的。对JS来说,大部分情况下编译发生在代码执行前的几微秒(甚至更短)的时间里。
简单来说,任何JS代码片段在执行之前都要进行编译(通常就在执行前)。因此,JS编译器首先会对var a = 2;这段程序进行编译,然后做好准备执行它,通常马上就会执行它。

我们知道了JS引擎会在解释JS代码之前首先对其进行编译,编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将他们关联起来,因此,正确的思路是:包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。JS实际上会把 var a = 2; 看成2个声明:var a;a = 2;,第一个定义声明是在编译阶段进行的,第二个赋值声明会被留在原地等待执行阶段。这个过程就好像变量和函数声明从它们代码中出出现的位置被“移动”到了最上面,这个过程就是 “提升”。值得注意的是:每个作用域都会进行提升操作,但并不是提升到整个程序的最上面。

变量声明提升

例1:

1
2
3
a = 2;
var a;
console.log(a);

你认为会输出什么结果呢?undefined吗?
其实这里会输出2。

经过变量声明提升后:

1
2
3
var a;
a = 2;
console.log(a); // 2

例2:

1
2
console.log(a);
var a = 2;

这段代码又会输出什么结果呢?是2 ?还是 ReferenceError ?
其实这里会输出 undefined。

经过变量声明提升后:

1
2
3
var a;
console.log(a); // undefined
a = 2;

函数声明提升

1
2
3
4
5
foo();
function foo() {
console.log(a); // undefined
var a = 2;
}

由于函数声明的提升,此代码可正常运行。

1
2
3
4
5
// 函数声明
function foo1 () {
// ...
}
foo1(); // 正常运行
1
2
3
4
5
// 函数表达式
foo2(); // 不是 ReferenceError,而是 TypeError
var foo2 = function bar() {
// ...
}

可以看到,函数声明会被提升,但函数表达式却不会被提升。
变量标识符foo2()被提升并分配给所在作用域(这里是全局作用域),因此foo2()不会导致 ReferenceError ,但是foo2此时并没有赋值(如果它是一个函数声明而不是函数表达式,那么就会赋值)。foo2()由于对 undefined 值进行函数调用而导致非法操作,因此抛出 TypeError异常。

同时也要记住,即使是具名的函数表达式,名称标识符在复制之前也无法在所在作用域中使用。

1
2
3
4
5
foo(); // TypeError
bar(); // ReferenceError
var foo = function bar() {
// ...
}

上述代码会被识别为:

1
2
3
4
5
6
7
var foo;
foo();
bar();
foo = function {
var bar = ... self ...
// ...
}

函数优先原则

函数声明和变量声明都会被提升,若两者名称相同,则函数首先被提升,然后才是变量。

1
2
3
4
5
6
7
8
9
10
11
foo();
var foo;

function foo() {
console.log(1);
}

foo = function() {
console.log(2);
}
此段代码将输出 1 。

尽管 var foo; 出现在 function foo() 的声明之前,但它是重复声明(因此被忽略了),因为函数的声明会被提升到普通变量之前。

上述代码会被识别为:

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

重复的var声明会被忽略,但出现在后面的函数声明还是可以覆盖前面的函数声明的。

1
2
3
4
5
6
7
8
9
10
11
12
13
foo(); // 3

function foo() {
console.log(1);
}

foo = function() {
console.log(2);
}

function foo() {
console.log(3);
}

因此,要避免代码里重复声明。