写了多年 JS 却仍被“变量提升”拿捏?这篇文章一次讲透

写了多年 JS 却仍被"变量提升"拿捏?这篇文章一次讲透

很多人写 JS 三五年,面试一问"变量提升是什么",张口就是:

"就是变量声明提升到作用域顶部嘛。"

听起来没错,但再往下问------

varletconst 的提升有啥区别?

函数表达式和函数声明谁提升得更高?

暂时性死区到底是啥?

为什么 var a = 10 提升后 aundefined 而不是 10

往往就开始含糊其辞了。

这篇文章不背八股,我们从 JS 真正执行前的那一步------编译阶段​ 说起,把变量提升这件事彻底拆清楚。


一、变量提升的本质:不是"搬家",而是"登记"

很多人以为"提升"是浏览器把代码物理地挪到顶部,其实完全不是。

真相是:JS 在执行代码前,会先进行一次"编译扫描",在这个阶段完成变量和函数的"绑定/注册"。

这个过程叫做 声明阶段(Declaration Phase) ,真正执行才是 赋值阶段(Assignment Phase)

ini 复制代码
console.log(a);
var a = 10;

发生了什么?

  1. 编译阶段 :JS 引擎扫描代码,发现 var a,于是在当前作用域的变量环境中登记一个变量 a,初始值为 undefined
  2. 执行阶段 :走到 console.log(a),读取 a,此时它还是 undefined
  3. 再往后执行到 a = 10,才给 a 赋值为 10

所以你看到的"提升",本质上是:

声明在编译期完成,赋值在执行期完成。


二、var:最容易被误解的提升

1. var 的"真面目"

ini 复制代码
console.log(a); // undefined
var a = 10;

等价于:

less 复制代码
var a;          // 编译阶段:声明 + 初始化为 undefined
console.log(a); // undefined
a = 10;         // 执行阶段:赋值

关键点有三个:

  • 声明提升
  • 自动初始化为 undefined
  • 赋值不提升

这也是为什么你能访问到一个"已声明但未赋值"的变量,而不会直接报错。

2. var 的函数级作用域

scss 复制代码
function test() {
  console.log(a);
  if (false) {
    var a = 10;
  }
}
test(); // undefined

哪怕 if (false) 永远不会执行,var a 依然会被提升,并且属于整个函数作用域


三、函数提升:比变量提升"更强势"

1. 函数声明:整体提升

scss 复制代码
foo();

function foo() {
  console.log('hello');
}

✅ 正常运行。

因为函数声明在编译阶段就已经完成了 声明 + 赋值(指向函数体)。

等价于:

scss 复制代码
function foo() {
  console.log('hello');
}
foo();

2. 函数表达式:按 var 规则来

javascript 复制代码
foo(); // TypeError: foo is not a function

var foo = function () {
  console.log('hello');
};

这里 foo 的提升规则和 var 一模一样:

javascript 复制代码
var foo;        // 编译阶段:foo = undefined
foo();          // 执行阶段:undefined(),直接崩
foo = function () { ... }; // 后面才赋值

⚠️ 注意错误类型:

  • ReferenceError:变量根本没声明
  • TypeError:变量存在,但类型不对(比如拿 undefined 当函数调用)

3. 优先级:函数声明 > 变量声明

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

var foo = 'bar';

function foo() {}

输出的是:

javascript 复制代码
ƒ foo() {}

原因:

编译阶段,函数声明和变量声明都会被提升,但 函数声明优先级更高,会覆盖同名变量的"占位"。

不过如果变量有赋值,执行阶段还是会覆盖回去:

javascript 复制代码
console.log(foo); // ƒ foo() {}

var foo = 'bar';

function foo() {}

console.log(foo); // 'bar'

四、let / const:真的"没有提升"吗?

这是最多人误解的地方。

1. TDZ(暂时性死区)现身

ini 复制代码
console.log(a); // ReferenceError
let a = 10;

于是很多人得出结论:let 不存在提升。

这个结论并不准确。

事实上:

let / const 同样存在提升,只是没有被自动初始化。

ES6 规范中引入了 Temporal Dead Zone(TDZ,暂时性死区)

  • 从作用域开始,到 let / const 声明语句之前
  • 这段时间内,变量虽然已经存在于作用域中
  • 无法访问,也无法读写
javascript 复制代码
{
  // TDZ 开始
  console.log(a); // ❌
  let a = 10;     // TDZ 结束
}

2. let vs var 的核心差异

特性 var let / const
是否提升
自动初始化 ✅ undefined
作用域 函数级 块级
TDZ

一句话总结:

var 提升并初始化;let / const 提升但不初始化。

3. const 的特殊点

arduino 复制代码
const a; // SyntaxError

const 不仅没有自动初始化,而且 必须在声明时赋值,否则直接语法报错。


五、经典面试题逐题拆解

题目 1:连环坑

ini 复制代码
var a = 1;
function foo() {
  console.log(a);
  var a = 2;
}
foo();

答案:undefined

解析:

  • foo 内部有 var a
  • 函数作用域内 a 被提升,初始化为 undefined
  • 遮蔽了外层的全局 a
  • 所以打印的是 undefined

题目 2:混合提升

ini 复制代码
console.log(typeof a);
var a = 1;
let b = 2;

答案:

javascript 复制代码
'undefined' // typeof a
ReferenceError // console.log(b)

解析:

  • var a 提升并初始化为 undefinedtypeof 不会报错
  • let b 处于 TDZ,typeof b 也会直接抛错(这点在老面试官眼里是加分项)

题目 3:循环中的 var vs let

javascript 复制代码
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 3 3 3

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 0 1 2

原因:

  • var i:整个循环共享同一个 i
  • let i:每次迭代都创建一个新的块级作用域 i
  • 这正是 块级作用域 + 提升规则 的典型实战体现

六、为什么会有变量提升?(设计层面的原因)

理解了"是什么",我们再往前走一步,看看"为什么"。

1. 函数互相调用的需要

csharp 复制代码
function a() {
  b();
}

function b() {
  a();
}

如果没有提升,无论哪个函数写在前面,另一个都会找不到。

提升机制让函数在编译阶段就全部就绪,支持互相调用。

2. 历史包袱

JS 最初被设计为"在浏览器里跑几行的小脚本语言",

设计者为了降低上手门槛,选择了:

  • 变量自动初始化
  • 容错而非严格报错

这些决策在当时是合理的,但也留下了今天的"坑"。


七、现代 JS 的最佳实践

既然搞懂了提升,那在实际项目中该怎么写?

✅ 1. 默认使用 let / const

  • 拒绝 var
  • 明确块级作用域
  • 避免 TDZ 之外的隐式 bug

✅ 2. 先声明,后使用

即使 let 允许你"技术上"在下面声明,也不要这么做:

ini 复制代码
// bad
console.log(a);
let a = 10;

// good
let a = 10;
console.log(a);

✅ 3. 函数优先使用表达式

javascript 复制代码
// good
const greet = () => {
  console.log('hi');
};

相比函数声明:

  • 提升行为更可控
  • 符合 const 的不可重复赋值语义
  • 更容易配合模块化和 tree-shaking

八、一句话总结

变量提升不是"代码搬家",而是"编译期登记"。

  • var:声明 + 自动初始化 → 可提前访问,值是 undefined
  • let / const:声明提升,但无自动初始化 → TDZ 内访问直接报错
  • 函数声明:整体提升,优先级高于变量
  • 函数表达式:按 var 规则提升

当你不再把"提升"当成一句口诀,而是能从 编译 → 执行 → 作用域 → TDZ​ 这一整条链路去解释它时,

你就真正跨过了"写 JS 多年却不懂变量提升"的那道坎。

相关推荐
大勇前进2 小时前
闭包到底有什么用?别只背概念,看完秒懂实际业务场景
后端
神奇小汤圆3 小时前
深入理解 TiDB 分布式事务:Percolator 模型与工程实践
后端
神奇小汤圆3 小时前
Flink SQL 从编写到提交运行的全过程解析
后端
饼干哥哥3 小时前
Codex 必改的8 个基础配置
前端·人工智能·后端
lingran__3 小时前
Linux 基础常用指令万字详解(上)|文件目录命令,结合底层原理剖析
linux·运维·服务器·后端·centos·linux基础指令
情可轻 秦任之4 小时前
ASP.NET页面优化,性能提升8倍的方法
后端·asp.net
王中阳Go4 小时前
用TRAE Work批量优化学员简历,原来2天的活现在2小时就干完了
后端·面试·agent
Zane19944 小时前
同一个 list 为什么能被两个 for 循环同时遍历?一文讲透可迭代对象与迭代器的真相
后端·python
他们叫我秃子4 小时前
前端开发转 Go 全栈(四):代码写在前面,却要最后执行?我终于搞懂了 defer
前端·后端·go