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

相关推荐
水银嘻嘻1 小时前
12 web 自动化之基于关键字+数据驱动-反射自动化框架搭建
运维·前端·自动化
it_remember1 小时前
新建一个reactnative 0.72.0的项目
javascript·react native·react.js
小嘟嚷ovo2 小时前
h5,原生html,echarts关系网实现
前端·html·echarts
十一吖i2 小时前
Vue3项目使用ElDrawer后select方法不生效
前端
只可远观2 小时前
Flutter目录结构介绍、入口、Widget、Center组件、Text组件、MaterialApp组件、Scaffold组件
前端·flutter
周胡杰2 小时前
组件导航 (HMRouter)+flutter项目搭建-混合开发+分栏效果
前端·flutter·华为·harmonyos·鸿蒙·鸿蒙系统
敲代码的小吉米2 小时前
前端上传el-upload、原生input本地文件pdf格式(纯前端预览本地文件不走后端接口)
前端·javascript·pdf·状态模式
是千千千熠啊2 小时前
vue使用Fabric和pdfjs完成合同签章及批注
前端·vue.js
da-peng-song3 小时前
ArcGIS Desktop使用入门(二)常用工具条——数据框工具(旋转视图)
开发语言·javascript·arcgis
九月TTS3 小时前
TTS-Web-Vue系列:组件逻辑分离与模块化重构
前端·vue.js·重构