任何声明在某个作用域内的变量,都将附属于这个作用域。但是作用域同其中的变量声明出现的位置有某种微妙的联系。这就涉及到JavaScript中的一个很重要的特性:变量提升。我们来详细的看下变量提升的奥秘。
直觉上会认为JavaScript代码在执行上是一行一行从上往下执行的。但是有一种特殊情况会导致这种假设是错误的。来看这块代码,并猜想输出:
1 | a = 2; |
这段代码会输出啥?会是undefined吗?其实真正输出结果是2。
考虑另一段代码:
1 | console.log(a); |
这段代码又会输出啥?会输出2吗?其实真正输出是undefined。这是为啥?
在前面分析作用域的时候,我们知道在执行之前有一个很重要的角色,那就是编译器。在执行之前编译器会进行编译。编译中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来。因此,正确的思路是,包括变量和函数在内的声明都会在代码被执行前首先被处理。
当看到var a = 2;
时,JavaScript实际上会分解为两个操作:var a;
和a = 2;
。第一个定义声明是在编译阶段进行的;第二个赋值声明会被留在原地等待执行阶段。所以,我们用编译器的角度来分析下第一段代码,可以重写为:
1 | var a; |
这样就很明显变量a
被赋值后输出,所以输出2.
再看第二段代码,等价为:
1 | var a; |
在变量a
赋值之前进行了输出,所以会输出undefined。
编译这个过程好比变量和函数的声明从他们的代码中被移动到了作用域的最上面。这个过程就叫做提升。即先声明,后赋值。
注意:只有声明本身会被提升,而赋值操作或其他逻辑会留在原地。
同理,函数声明会被提升,函数表达式却不会提升。比如:
1 | console.log(foo); // foo函数 |
函数声明和变量声明都会被提升,但是需要注意的是,函数会首先被提升,然后才是变量(函数是第一共公民🙂)。
1 | console.log(foo); // foo函数 |
尽管变量foo的声明在函数foo的声明之前,但是因为函数声明会首先被提升,所以先声明了foo函数,当遇到变量foo的声明时,因为已经有了声明,所以重复的声明会被编译器忽略掉。所以上面的代码在编译器视角是这样子的:
1 | function foo() { |
注意:虽然重复的var声明会被忽略,但是出现在后面的函数声明还是可以覆盖前面的声明的。
一个普通块内部的函数声明通常会被提升到所在作用域的顶部,这个过程不会被条件判断等影响,所以需要避免在块作用域的逻辑内部进行声明函数。避免在一个作用域内部重复声明,否则会引起很多奇怪的问题。
参考自:《你不知道的JavaScript(上卷)》