引言:一个奇怪的现象
先来看两段看似矛盾的代码:
代码1
javascript
a = 2;
var a;
console.log(a); // 输出2,而不是undefined!
代码2
javascript
console.log(a); // 输出undefined,而不是报错!
var a = 2;
为什么声明在后面的变量却能影响前面的代码?这就是JavaScript的**变量提升(Hoisting)**在"搞鬼"------它让声明(蛋)总是比赋值(鸡)先出现!
一、变量提升的本质
1.1 编译阶段的"乾坤大挪移"
JavaScript引擎在代码执行前会先进行编译。此时,所有变量和函数的声明都会被"提升"到当前作用域的顶部,而赋值操作则留在原地。
原代码:
javascript
a = 2;
var a;
console.log(a);
实际执行顺序:
javascript
var a; // 声明被提到最前
a = 2; // 赋值留在原地
console.log(a); // 2
1.2 未赋值的变量去哪了?
javascript
console.log(a); // undefined,而不是报错!
var a = 2;
实际执行顺序:
javascript
var a; // 声明提升
console.log(a); // 此时a还未赋值
a = 2; // 赋值仍在原地
未赋值时,变量默认值为undefined
,因此不会抛出ReferenceError
。
这个过程就好像变量和函数声明从它们在代码中出现的位置被"移动"到了最上面。这个过程就叫做提升。 换句话说,先有蛋(声明)后有鸡(赋值)。
二、函数提升 vs 函数表达式
2.1 函数声明:整体提升
只有声明本身会被提升,而赋值或者其他运行逻辑会留在原地等待执行。如果提升改变了代码执行的顺序,会造成非常严重的后果。
javascript
foo(); // 正常执行,输出undefined
function foo() {
console.log(a);
var a = 2;
}
foo函数的声明(这个例子还包括实际函数的隐含值)被提升了,因此第一行中的调用可以正常执行。
实际执行顺序:
每个作用域都会进行提升操作。上面的foo函数自身也会在内部对var a进行提升(显然并不是提升到了整个程序的最上方)。因此,这段代码实际上相当于:
javascript
function foo() { // 函数整体提升
var a; // 内部变量提升
console.log(a); // undefined
a = 2;
}
foo();
2.2 函数表达式:只提升变量,不提升函数
ps:不是以function开头则是函数表达式。
javascript
foo(); // TypeError,不是ReferenceError
var foo = function bar() {
// ...
};
可以看到,函数声明会被提升,但是函数表达式却不会被提升。
另一个小细节:
javascript
var foo;
foo();//TypeError
bar();//ReferenceError
foo=function bar() {
//...
};
上面代码中,foo被提升了,但是foo指向的函数表达式bar却没有被提升。
函数表达式赋值给变量时,只有变量声明被提升,函数本身不会提前存在。
三、函数与变量提升的优先级
3.1 函数优先
函数声明和变量声明都会被提升,但是函数会首先被提升,然后才是变量。
javascript
foo(); // 输出1,而不是2!
var foo;
function foo() {
console.log(1);
}
foo = function() {
console.log(2);
};
实际执行顺序:
javascript
function foo() { // 函数声明优先提升
console.log(1);
}
var foo; // 重复声明被忽略
foo(); // 调用原函数
foo = function() { ... }; // 后续赋值覆盖
注意var foo尽管出现在function foo()...的声明之前,但它是重复声明(因此被忽略了),因为函数声明会被提升到普通变量之前。
ps:var在编译阶段就声明了,函数在执行阶段运行,所以函数一定会覆盖同名的变量声明。
3.2 重复声明的陷阱
尽管重复的var声明会被忽略掉,但是出现在后面的函数声明还是可以覆盖前面的。
javascript
foo(); // 输出3,而非2!
var foo = function() { console.log(1); };
function foo() { console.log(2); };
var foo = function() { console.log(3); };
实际顺序:
javascript
function foo() { console.log(2); } // 函数优先
var foo; // 被忽略
var foo; // 被忽略
foo = function() { console.log(1); }; // 被后续覆盖
foo = function() { console.log(3); };
foo(); // 最终调用最后一个赋值
四、块级作用域的坑:别在块内声明函数!
一个普通块内部的函数声明通常会被提升到所在作用域的顶部,这个过程不会像下面的代码暗示的那样可以被条件判断所控制:
javascript
if (true) {
function foo() { console.log("块内函数"); }
}
foo(); // 有时能调用,有时报错!
-
问题:ES5中块内的函数声明会被提升到外层作用域,但不同浏览器处理方式不同。
-
解决方案 :始终使用函数表达式或ES6的
let/const
:javascriptif (true) { const foo = function() { ... }; // 安全! }
总结:如何避免提升带来的问题?
- 始终先声明后使用:手动将变量和函数声明放在作用域顶部。
- 使用
let/const
替代var
:ES6的块级作用域能有效避免提升的迷惑行为。 - 函数优先用表达式:避免依赖函数声明的提升特性。
- 工具辅助:ESLint等工具可检测可疑的提升用法。
下次看到var a = 2;
时,不妨默念:"声明已上天,赋值留人间",就能轻松理解JavaScript的"蛋鸡哲学"啦! 🐣