JavaScript中的“先有鸡还是先有蛋”——变量提升的奥秘

引言:一个奇怪的现象

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

代码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:

    javascript 复制代码
    if (true) {
        const foo = function() { ... }; // 安全!
    }

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

  1. 始终先声明后使用:手动将变量和函数声明放在作用域顶部。
  2. 使用let/const替代var:ES6的块级作用域能有效避免提升的迷惑行为。
  3. 函数优先用表达式:避免依赖函数声明的提升特性。
  4. 工具辅助:ESLint等工具可检测可疑的提升用法。

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

相关推荐
小呆呆66617 分钟前
副业搞起来,小说,漫画,漫剧的成本优化思路
前端·后端·面试
凤城老人32 分钟前
从 PyQt6 到 Electron:给 Edge TTS 做一个“多角色配音机“的踩坑手记
javascript·typescript·electron
Dovis(誓平步青云)36 分钟前
浇水提醒刚弹出又消失,植物状态别只存一个百分比
开发语言·前端·javascript·pdf·ecmascript·电脑
码艺-Alimjan1 小时前
Vben Admin 新增维吾尔语 Vben-Modal的关键坑之一
前端·javascript·vue.js
可乐鸡翅yeah_2 小时前
hls.js 手动自定义 http 请求 loader,修改请求头实战
开发语言·前端·javascript·网络协议·http·ecmascript·m3u8在线
IT_陈寒2 小时前
Vite静态资源导入这个坑我帮你们踩过了
前端·人工智能·后端
广州华水科技2 小时前
大坝安全监测解决方案:单北斗GNSS形变监测系统应用与维护
前端
yivifu3 小时前
中文古籍电子书注释集成
前端·javascript·python·beautifulsoup·epub
weixin_448290253 小时前
书庐开发实战教学
javascript·css·html
默_笙4 小时前
🛴 从散件到整机:DeepAgents 与 Agent 身上预留的那些"插槽"(前置介绍)
前端·javascript