JavaScript中的“先有鸡还是先有蛋”——变量提升的奥秘
2025-04-05 09:37 阅读(42)

引言:一个奇怪的现象

先来看两段看似矛盾的代码:


代码1

a = 2;
var a;
console.log(a); // 输出2,而不是undefined!

代码2

console.log(a); // 输出undefined,而不是报错!
var a = 2;

为什么声明在后面的变量却能影响前面的代码?这就是JavaScript的**变量提升(Hoisting)**在“搞鬼”——它让声明(蛋)总是比赋值(鸡)先出现!


一、变量提升的本质

1.1 编译阶段的“乾坤大挪移”

JavaScript引擎在代码执行前会先进行编译。此时,所有变量和函数的声明都会被“提升”到当前作用域的顶部,而赋值操作则留在原地。

原代码:

a = 2;
var a;
console.log(a);

实际执行顺序:


var a;    // 声明被提到最前
a = 2;    // 赋值留在原地
console.log(a); // 2

1.2 未赋值的变量去哪了?

console.log(a); // undefined,而不是报错!
var a = 2;

实际执行顺序:


var a;          // 声明提升
console.log(a); // 此时a还未赋值
a = 2;          // 赋值仍在原地

未赋值时,变量默认值为undefined,因此不会抛出ReferenceError。

这个过程就好像变量和函数声明从它们在代码中出现的位置被“移动”到了最上面。这个过程就叫做提升。 换句话说,先有蛋(声明)后有鸡(赋值)。


二、函数提升 vs 函数表达式

2.1 函数声明:整体提升

只有声明本身会被提升,而赋值或者其他运行逻辑会留在原地等待执行。如果提升改变了代码执行的顺序,会造成非常严重的后果。

foo(); // 正常执行,输出undefined
function foo() {
    console.log(a);
    var a = 2;
}

foo函数的声明(这个例子还包括实际函数的隐含值)被提升了,因此第一行中的调用可以正常执行。

实际执行顺序:

每个作用域都会进行提升操作。上面的foo函数自身也会在内部对var a进行提升(显然并不是提升到了整个程序的最上方)。因此,这段代码实际上相当于:

function foo() {  // 函数整体提升
    var a;        // 内部变量提升
    console.log(a); // undefined
    a = 2;
}
foo();

2.2 函数表达式:只提升变量,不提升函数

ps:不是以function开头则是函数表达式。


foo(); // TypeError,不是ReferenceError
var foo = function bar() {
    // ...
};

可以看到,函数声明会被提升,但是函数表达式却不会被提升。


另一个小细节:

var foo;
foo();//TypeError 
bar();//ReferenceError 

foo=function bar() {
    //... 
};

上面代码中,foo被提升了,但是foo指向的函数表达式bar却没有被提升。

函数表达式赋值给变量时,只有变量声明被提升,函数本身不会提前存在。


三、函数与变量提升的优先级

3.1 函数优先

函数声明和变量声明都会被提升,但是函数会首先被提升,然后才是变量。

foo(); // 输出1,而不是2!
var foo;
function foo() {
    console.log(1);
}
foo = function() {
    console.log(2);
};

实际执行顺序:


function foo() {  // 函数声明优先提升
    console.log(1);
}
var foo;          // 重复声明被忽略
foo();            // 调用原函数
foo = function() { ... }; // 后续赋值覆盖

注意:var foo尽管出现在function foo()...的声明之前,但它是重复声明(因此被忽略了),因为函数声明会被提升到普通变量之前。


3.2 重复声明的陷阱

尽管重复的var声明会被忽略掉,但是出现在后面的函数声明还是可以覆盖前面的

foo(); // 输出3,而非2!
var foo = function() { console.log(1); };
function foo() { console.log(2); };
var foo = function() { console.log(3); };

实际顺序:


function foo() { console.log(2); } // 函数优先
var foo;                           // 被忽略
var foo;                           // 被忽略
foo = function() { console.log(1); }; // 被后续覆盖
foo = function() { console.log(3); };
foo(); // 最终调用最后一个赋值

四、块级作用域的坑:别在块内声明函数!

一个普通块内部的函数声明通常会被提升到所在作用域的顶部,这个过程不会像下面的代码暗示的那样可以被条件判断所控制:

if (true) {
    function foo() { console.log("块内函数"); }
}
foo(); // 有时能调用,有时报错!

问题:ES5中块内的函数声明会被提升到外层作用域,但不同浏览器处理方式不同。

解决方案:始终使用函数表达式或ES6的let/const:

if (true) {
    const foo = function() { ... }; // 安全!
}

总结:如何避免提升带来的问题?


始终先声明后使用:手动将变量和函数声明放在作用域顶部。

使用let/const替代var:ES6的块级作用域能有效避免提升的迷惑行为。

函数优先用表达式:避免依赖函数声明的提升特性。

工具辅助:ESLint等工具可检测可疑的提升用法。


下次看到var a = 2;时,不妨默念:“声明已上天,赋值留人间”,就能轻松理解JavaScript的“蛋鸡哲学”啦!


作者:海底火旺

链接:https://juejin.cn