到现在,我们已经明白作用域的概念了,以及根据声明的位置和方式将变量分配给作用域的相关原理。函数作用域和块作用域的行为是一样的,可以总结为:任何声明在某个作用域内的变量,都将附属于这个作用域。
但是,作用域同其中的变量声明出现的位置有某种微妙的联系,而这个细节正是我们将要讨论的内容。
直觉上会认为 JavaScript 代码在执行时是由上到下一行一行执行的。但是实际上这并不是完全正确,有一种特殊情况会导致这个假设错误。
a = 2;
var a;
console.log(a);
很多人认为这里会是 undefined,因为 var a; 在 a = 2 之后,我们就自然而然地认为,变量被重新赋值了,因此会被赋值为 undefined,但是,这里真正地输出结果是 2
再看这段代码,鉴于上一段代码片段表现出来的某种非自上而下的特点,你很可能认为这里应该输出 2,还有人认为由于变量 a 在使用前没有声明,因此会抛出 ReferenceError 异常。
console.log(a);= 2;
var a = 2;
这两种猜测都不对,输出会是 undefined
那么到底发生了什么,看起来我们面对的是一个先有鸡还是先有蛋的问题,是声明在前,还是赋值在前?
第一章介绍了编译器,我们知道引擎在解释代码前会进行编译
编译阶段一部分工作就是找到所有声明然后用作用域和他们关联起来
第二章展示了这种机制,这也正是词法作用域的核心内容
这里正确的思路是:包括变量和函数在内的所有声明都会在任何代码被执行之前首先被处理
当我们看到 var a = 2 时,可能会认为这是一个声明,但实际上,JavaScript 会认为这是两个声明,var a 和 a = 2,第一个定义声明在编译阶段进行,第二个赋值声明在原地等待执行阶段。
// 编译前
a = 2;
var a;
console.log(a);
// 编译后
var a;
a = 2;
console.log(a);
// 编译前
console.log(a);= 2;
var a = 2;
// 编译后
var a;
console.log(a);
a = 2;
这个过程就好像是变量和函数声明从它们地代码出现的位置被劫持到了最上面,这个过程就叫作提升。
换句话说,先有蛋(声明)后又鸡(赋值)
foo(); // 这里不会报错,因为 foo 函数声明提升了
function foo() {
console.log(a); // undefined
var a = 2;
}
foo(); // TypeError
var foo = function() {
console.log('123');
}
第二段代码可以看到,函数声明会被提升,但是函数表达式不会被提升
为什么是 TypeError 而不是 ReferenceError 呢?因为 var foo 会提升,但是类型是不确定的
函数声明和变量声明都会被提升,但是函数会首先提升,然后才是变量
foo(); // 1
var foo;
function foo() {
console.log(1);
}
foo = function() {
console.log(2);
}
这里尽管 var foo 出现在 function foo() 之前,但是它是重复声明,这里会被忽略掉,因为函数声明会被提升到普通变量之前。
foo(); // 3
var foo;
function foo() {
console.log(1);
}
foo = function() {
console.log(2);
}
function foo() {
console.log(3);
}
我们习惯将 var a = 2; 看作是一个声明,而实际上 JavaScript 引擎并不是这么认为,它将 var a 和 a = 2 当作两个单独的声明,第一个是编译阶段的任务,而第二个是执行阶段的任务。
这意味着无论作用域的声明出现在什么位置,都将在代码本身被执行前被首先执行,可以将这个过程形象的想象成所有的声明都会被移动到各自作用域的最顶端,这个过程被称为提升。
声明本身会被提升,而包括函数表达式的赋值在内的赋值操作并不会提升。
要注意避免重复声明,特别是当普通的 var 声明和函数声明混合在一起的时候吗,否则会引起很多危险的问题!