1. 提升现象
直觉上认为JavaScript代码在执行时是由上到下一行一行执行的。但实际上这并不完全正确。
思考如下代码:
a = 2;
var a;
console.log(a); // 这里会打印什么呢?
可能会认为会输出undefined
,因为var a
声明在a = 2
之后,会自然而然的认为变量被重新赋值了,因此会赋予默认值undefined
。但是这里输出的结果是2。
考虑如下代码:
console.log(a);
var a = 2;
这里会输出什么呢?
根据第一个代码片段可能会认为输出为2
。或者由于变量a
在使用前没有进行声明,因此会抛出错误。
到底哪个结论正确呢?
结果是两种猜测都不对,这里输出的是undefined
。
2. 提升的原因
引擎会在解释JavaScript代码之前首先对其进行编译。编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将他们关联起来,这也是前面讲到的词法作用域
的核心内容。
因此正确的思考思路是,包括变量和函数在内的所有声明都会在任何代码执行前首先处理
。
当看到var a = 2;
时,可能会认为这是一个声明。但JavaScript引擎实际上会将其看做两个声明:var a;
和a = 2;
。第一个定义声明是在编译阶段进行的。第二个赋值声明会被保留在原地等待执行阶段。
对代码进行如下处理:
var a;
a = 2;
console.log(a); // 2
第一部分是编译,第二部分是执行。
类似的对于第二段代码:
var a;
console.log(a); // undefined
a = 2;
这个过程就好像变量和函数声明从它们所在的代码位置被移动
到了最前面。这个过程就叫做提升
。
注意:只有声明本身会被提升,而赋值或其他运行逻辑会留在原地。如果提升改变了代码的执行顺序,那会造成代码运行的混乱。
foo();
function foo() {
console.log(a); // undefined
var a = 2;
}
foo
函数的声明被提升了,因此在第一行调用可以正常进行。
每个作用域都会进行提升操作,foo
函数自身会在内部对var a;
进行提升。因此上面这段代码可以理解为如下:
function foo(){
var a;
console.log(a);
a = 2;
}
foo();
注意:函数声明会被提升,但是函数表达式不会被提升
foo(); // 这里会报错
var foo = function bar() {...}
这段程序中foo()
被提升到所分配的作用域,因此foo()
不会导致错误,但是foo
此时还没有被赋值。foo()
相当于undefined()
,因此会抛出异常。
同时即使使用具名的函数表达式,名称标识符在赋值之前也无法在所在的作用域使用
foo(); // 报错
bar(); // 报错
var foo() = function bar() {...} // 具名函数
这段代码可以理解为如下代码:
var foo;
foo(); // 报错
bar(); // 报错
foo = function() {...}
3. 函数优先
上面说到函数声明和变量声明都会被提升,但是函数首先会被提升然后是变量。
分析如下代码:
foo(); // 1
var foo;
function foo() {
console.log(1);
}
foo = function() {
console.log(2);
}
这里执行foo
会输出1
而不是2
。
解析代码:
function foo() {
console.log(1);
}
foo(); // 1
foo = function () {
console.log(2);
}
尽管var foo
出现在function foo() {...}
之前,但是它是重复的声明(因此被忽略了),因为函数声明会被提升到变量声明之前。
尽管重复的var foo
声明会被忽略掉,但后面的函数声明还是可以覆盖前面的。
foo(); // 3
function foo() {
console.log(1);
}
var foo = function (){
console.log(2);
}
function foo() {
console.log(3);
}
注意:一个普通的块内部的函数声明通常会被提升到所在的作用域顶部,这个过程不会像下面代码暗示的那样被条件语句控制。
foo(); // 报错
if(true) {
function foo(){
console.log(1);
}
}else {
function foo(){
console.log(2);
}
}
这种情况在JavaScript未来版本可能会发生改变,但是要尽可能避免在块内部使用。
4. 小结
我们可能习惯的将var a = 2;
当做一个声明,在实际中JavaScript引擎会将var a;
和a = 2
当做两个单独的声明,第一个是编译阶段的任务,第二个是执行阶段的任务。
这就意味着无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理,所有声明的变量和函数都会被移动
到各自作用域的最顶端,这个过程就是提升。声明本身会被提升,而包括函数表达式的赋值在内的赋值操作并不会提升。
要避免重复声明,特别是当普通的var声明和函数声明混合在一起的时候,否则会引起一些意想不到的问题。