JS 变量提升:代码没动,为什么执行顺序就变了?

从一个反直觉的面试题说起

你有没有见过这种面试题?面试官拿出一段代码,让你说出会输出什么,然后你自信满满地写下答案,结果面试官摇摇头说:"错了,回去好好补补基础。"

javascript 复制代码
showName();
console.log(myName);

var myName = 'DMYX';

function showName(){
    console.log('函数showName被执行了');
}

要是你以前没见过变量提升,大概率会觉得:这不是应该报错吗?showName 还没定义就调用了,myName 也是先打印后赋值。

但实际运行起来,结果是这样的:

javascript 复制代码
函数showName被执行了
undefined

是不是感觉 JavaScript 偷偷把代码顺序改了?就像你在餐厅排队,突然有个人插了队,而且还理直气壮地说:"我本来就应该在这儿。"

别急,这不是什么玄学。这事儿背后有个正经名字,叫变量提升(Hoisting)。而且更重要的是------你的代码其实一行都没被移动。


变量提升不是"物理移动",是"内存分配"

以前你可能听过这种解释:"变量提升就是把变量声明移到代码最前面。"

这话不能说全错,但容易让人误解------好像 JavaScript 引擎真的会去修改你的源代码,把 var myName 那行剪切粘贴到文件顶部似的。

实际上不是这样的。你的代码在文件里的位置,一个字节都没变。变量提升发生在编译阶段,是 JavaScript 引擎在内存里做的手脚。

JS 代码是怎么运行的?

以前你可能觉得 JS 就是一行一行按顺序执行的。但实际上,JS 代码的执行分为两个阶段:

  1. 编译阶段:JS 引擎(比如 Chrome 的 V8)快速扫描一遍代码,做一些准备工作
  2. 执行阶段:真正一行一行跑代码

变量提升,就是发生在编译阶段的事情。

举个例子,这段代码:

javascript 复制代码
console.log(myname);
var myname = 'DMYX';

function showName(){
    console.log('代码被执行了');
}
showName();

在编译阶段,JS 引擎会做这么几件事:

  • 遇到 var myname,就在内存里创建一个叫 myname 的变量,默认值是 undefined
  • 遇到 function showName(),就在内存里创建一个叫 showName 的变量,直接把整个函数对象存进去
  • 赋值操作(myname = 'DMYX')和函数调用(showName())这些,编译阶段统统不管

所以到了执行阶段,内存里已经准备好了这些东西。当执行到第一行 console.log(myname) 的时候,myname 已经存在了,值是 undefined,所以不会报错,而是打印 undefined


函数声明 vs 函数表达式:谁先"上位"?

现在问题来了:如果一个名字既是函数名又是变量名,谁先提升?

看这段代码:

javascript 复制代码
console.log(foo);
foo();

var foo = '我是变量';

function foo() {
    console.log('我是函数');
}

console.log(foo);

你觉得会输出什么?

答案是:

csharp 复制代码
[Function: foo]
我是函数
我是变量

为什么?因为函数声明的提升优先级比变量声明更高

在编译阶段,JS 引擎先处理函数声明,把 foo 指向函数对象。然后遇到 var foo,发现 foo 已经存在了,就不会再重新赋值为 undefined 了。

所以第一个 console.log(foo) 打印的是函数。然后调用 foo() 执行函数,打印"我是函数"。

到了执行阶段,遇到 var foo = '我是变量',这时候才会把 foo 重新赋值为字符串。所以最后一个 console.log(foo) 打印"我是变量"。

这就好比在公司里,你本来是个经理(函数声明),结果有个新人来报到,职位也是经理(变量声明)。老板一看,你已经是经理了,就不会让新人顶替你。但如果老板后来给你调岗了(赋值操作),那你就变成新职位了。

那如果是函数表达式呢?情况就不一样了:

javascript 复制代码
console.log(add(1, 2));
var add = function(a, b){
    return a + b;
}

这段代码会报错:TypeError: add is not a function

为什么?因为函数表达式本质上是一个赋值操作。编译阶段只会提升 var add,把它初始化为 undefined,而不会管右边的函数。所以到执行阶段,add 还是 undefined,调用它自然会报错。

这就好比你在公司里宣布"我要招聘一个经理"(变量声明),但人还没来(没赋值),你就让他干活,当然会出问题。


let/const 的反击:暂时性死区(TDZ)

ES6 出来之后,letconst 来了,它们带来了一个新东西叫暂时性死区(Temporal Dead Zone,简称 TDZ)

有人说:"let 和 const 没有变量提升。"

这话其实不准确。准确来说,let 和 const 也会被提升,但它们的内存空间是在词法环境 里,而不是变量环境里。而且在声明之前,这些变量处于"不可用"状态,只要你敢用,就报错。

看这段代码:

javascript 复制代码
console.log(myname);  // 报错:ReferenceError: myname is not defined
let myname = 'DMYX';

let 声明的变量,在声明之前访问会直接报错,而不是返回 undefined。这就是暂时性死区在起作用。

为什么要这么设计?因为变量提升虽然方便,但也容易写出混乱的代码。你可能在不知不觉中用到了还没赋值的变量,导致 bug。

letconst 的暂时性死区,就是为了让你养成"先声明,后使用"的好习惯。


总结:如何写出不被"提升"坑的代码

好了,讲了这么多,你可能会问:"既然变量提升这么容易坑人,那我该怎么写代码才能避免?"

给你几个建议:

  1. 尽量用 let/const,少用 var:这是最直接的办法。let 和 const 的块级作用域和暂时性死区,能帮你避免很多提升带来的问题。

  2. 养成"先声明,后使用"的习惯:不管有没有提升,把变量声明放在作用域顶部,代码会更清晰。

  3. 函数声明优先,函数表达式谨慎用:如果你想让函数能在声明前调用,就用函数声明;如果你想避免提升,就用函数表达式。

  4. 别在条件判断里声明函数:不同浏览器对这种情况的处理可能不一样,容易出兼容性问题。

变量提升就像 JavaScript 里的一个"隐藏机制",不懂的时候觉得是玄学,懂了之后发现其实是 JS 引擎在帮你做一些准备工作。

但现代 JavaScript 开发中,我们其实很少再去依赖变量提升了。有了 let/const,有了更好的编码规范,我们完全可以写出更清晰、更易维护的代码。

最后想问你一个问题:你在项目里遇到过变量提升带来的 bug 吗?如果有,欢迎在评论区分享你的故事。

相关推荐
yingyima1 小时前
MySQL 事件调度器速查:核心语法与实战代码
前端
GISer_Jing1 小时前
Claude Code多Agent架构深度剖析
前端·人工智能·架构·自动化
comphub1 小时前
comp-hub:让你的 Vue 业务组件真正"活"起来
前端
AI砖家1 小时前
Claude Code 跳过确认完全指南:让 AI 自己完成开发任务
前端·人工智能·python·ai编程·代码规范
KaMeidebaby2 小时前
卡梅德生物技术快报|Pull Down 实验在 lncRNA - 蛋白互作机制研究中的应用实例解析
大数据·前端·架构·spark·新浪微博
锋行天下2 小时前
让nginx网关扛下所有攻击
前端·后端·nginx
广州华水科技2 小时前
单北斗GNSS是什么?主要有哪些形变监测应用?
前端
就叫_这个吧2 小时前
JavaScript中常用事件示例展示附源码
开发语言·javascript·html
愿天垂怜2 小时前
【C++脚手架】gtest 单元测试库的介绍与使用
linux·服务器·c++·gitee·前端框架·gtest