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的"蛋鸡哲学"啦! 🐣

相关推荐
zhougl9961 小时前
html处理Base文件流
linux·前端·html
花花鱼1 小时前
node-modules-inspector 可视化node_modules
前端·javascript·vue.js
HBR666_1 小时前
marked库(高效将 Markdown 转换为 HTML 的利器)
前端·markdown
careybobo3 小时前
海康摄像头通过Web插件进行预览播放和控制
前端
TDengine (老段)3 小时前
TDengine 中的关联查询
大数据·javascript·网络·物联网·时序数据库·tdengine·iotdb
杉之4 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue
喝拿铁写前端4 小时前
字段聚类,到底有什么用?——从系统混乱到结构认知的第一步
前端
再学一点就睡4 小时前
大文件上传之切片上传以及开发全流程之前端篇
前端·javascript
木木黄木木5 小时前
html5炫酷图片悬停效果实现详解
前端·html·html5
请来次降维打击!!!6 小时前
优选算法系列(5.位运算)
java·前端·c++·算法