浅谈JS变量声明和函数声明提升
先来两个问题
很多时候,在直觉上,我们都会认为JS代码在执行时都是自上而下一行一行执行的,但是实际上,有一种情况会导致这个假设是错误的。
a = 2;
var a;
console.log(a);
按照传统眼光,console.log(a)输出的应该是undefined,因为var a在a = 2之后。但是,输出的是2。
再看第二段代码:
console.log(a);
var a = 2;
有人会想到第一段代码,然后回答undefined。还有人会认为a在使用前未被声明,因此抛出ReferenceError异常。遗憾的是,结果是undefined。
为什么呢?
从编译器的角度看问题
JS在编译阶段,编译器的一部分工作就是找到所有声明,并用合适的作用域将他们关联起来。对于一般人来说var a = 2仅仅是一个声明,但是,JS编译器会将该段代码拆为两段,即:var a和a = 2。var a这个定义声明会在编译阶段执行,而a = 2这个赋值声明会在原地等待传统意义上的从上到下的执行。
所以,在编译器的角度来看,第一段代码实际上是这样的:
var a; // 编译阶段执行
a = 2;
console.log(a);
所以,输出的是2。
类似的,第二个代码片段实际上是这样执行的:
var a;
console.log(a);
a = 2;
这样的话,很明显,输出的应该是undefined,因为只对a进行了定义声明,没有对a进行赋值声明。
从上面这两个例子可以看出,变量声明会从它们在代码中出现的位置被移动到当前作用域的最上方进行执行,这个过程叫做提升。
函数提升
下面,再来看一段代码
foo();
function foo () {
console.log(a);
var a = 2;
}
在这个例子中,输出undefined而不会报错,因为,函数变量也能提升。即,实际上像如下的情况运行。
function foo () {
var a;
console.log(a);
a = 2;
}
foo();
说到这里,你是不是认为提升很简单,只要把变量都放到当前作用域最上方执行就好了?
下面,我来说一种意外情况:函数表达式的提升情况。
函数表达式的提升情况
foo();
var foo = function bar () {
console.log(a);
var a = 2;
}
你是不是想说,这个例子不是和之前的那个差不多吗?输出的当然是undefined呀。但是,结果是,不输出,因为JS报了TypeError错误!
因为,函数表达式不会进行提升!
该例子的实际运行情况是这样的:
var foo;
foo();
foo = function bar () {
var a;
console.log(a);
a = 2;
}
由于执行时,在作用域中找得到foo(该作用域最上方声明了foo),所以不会报ReferenceError错误,但是,foo此时没有进行赋值(如果foo是一个函数声明而不是函数表达式,那么就会赋值),也就是说实际上foo()是对一个值为undefined的变量进行函数调用,所以,理所应当抛出TypeError异常。
值得一提的是,即使是具名的函数表达式,名称标识符在赋值之前也无法在所在作用域中使用,即:
foo(); // TypeError
bar(); // ReferenceError
var foo = function bar () {}
函数优先
函数声明和变量声明都会被提升,但是有一个值得注意的细节,那就是,函数会首先提升,然后才是变量!
看下面这一段代码:
foo();
var foo;
function foo () {
console.log(1);
}
foo = function () {
console.log(2);
}
这一段代码会输出1,原因就在于,函数优先。
这一段代码可以转换为以下形式:
function foo () {
console.log(1);
}
var foo; // 重复声明,被忽略
foo(); // 输出1
foo = function () {
console.log(2);
}
如果,在代码的结尾再执行一次foo函数,此时,输出的是1。
function foo () {
console.log(1);
}
var foo; // 重复声明,被忽略
foo(); // 输出1
foo = function () {
console.log(2);
}
foo(); // 输出2
因为,尽管重复的声明会被忽略了,但是后面的函数还是可以覆盖前面的函数。
明白了这个道理,你就可以理解下面这个问题了:
foo();
var a = true;
if (a) {
function foo () {
console.log("a");
}
} else {
function foo () {
console.log("b");
}
}
你猜这道题输出的结果是什么?是b!为什么?因为foo进行了两次的声明,但是,后一次函数覆盖了前一次的函数。所以调用foo时,永远调用的都是console.log("b")。
总结
1.所有声明(变量和函数)都会被移动到各自作用域的最顶端,这个过程被称为提升
2.函数表达式等各种赋值操作并不会被提升
3.函数优先原则
4.尽量避免产生提升问题