一文搞懂 JavaScript 变量声明:var、let、const 到底有什么区别?
前言
JavaScript 是一门弱类型的动态语言,诞生于网景(Netscape)时代,最初的设计目标很简单------给网页加点交互、做做幻灯片、操作一下 DOM。据说 Brendan Eich 只花了一周就写出了 JS 的雏形,妥妥的"KPI 项目"。
因为是赶工出来的,JS 早期设计上有很多不完美的地方,变量声明 就是其中之一。直到 ES6(ECMAScript 2015)发布,let 和 const 的出现才真正解决了 var 遗留下来的诸多坑。
今天就让我们从作用域、变量提升、循环陷阱等角度,一次性把 var、let、const 的区别讲透。
一、var 时代的问题
在 ES6 之前,声明变量只能用 var,连常量的概念都没有,全靠程序员的代码规范来约束:
js
var PI = 3.1415926; // 希望大家不要改它,但它其实可以改...
var 有两大核心缺陷:
1. 不支持块级作用域
js
var age = 100;
if (age > 12) {
var dog = age * 7; // 用 var 声明
let x = 111; // 用 let 声明
console.log(dog); // 700
}
console.log(dog); // 700 --- var 声明的变量"泄漏"到块外面了
console.log(x); // ReferenceError: x is not defined --- let 则不会
var 声明的变量无视 {} 代码块,直接穿透到外层。这在大型项目中极易造成变量污染。
2. 存在变量提升(Hoisting)
js
console.log(pizza); // undefined --- 不报错?!
var pizza = 'Deep Dish';
换成 let:
js
console.log(pizza); // ReferenceError: Cannot access 'pizza' before initialization
let pizza = 'Deep Dish';
这就是所谓的变量提升 ------var 声明的变量在编译阶段就被提升到作用域顶部并初始化为 undefined,导致代码行为与直觉不符。let 和 const 则不存在这种诡异的提升行为。
二、作用域(Scope)
理解作用域是掌握变量声明的关键。
三种作用域
| 作用域类型 | 说明 | var | let | const |
|---|---|---|---|---|
| 全局作用域 | 代码最外层 | ✅ | ✅ | ✅ |
| 函数作用域 | function 内部 |
✅ | ✅ | ✅ |
| 块级作用域 | { } 代码块内部 |
❌ | ✅ | ✅ |
一句话总结:var 只认函数,不认花括号;let 和 const 函数和花括号都认。
作用域嵌套与变量查找
JS 的变量查找遵循冒泡规则:
- 先在当前作用域中查找
- 找到了 → 直接用
- 没找到 → 向外层作用域冒泡查找
- 一直找到全局作用域还没找到 → 停下,报
ReferenceError
js
var height = 200; // 全局作用域
function setWidth() {
var width = 100; // 函数局部作用域
console.log(width); // 100 --- 在当前作用域找到了
console.log(height); // 200 --- 当前没找到,冒泡到全局找到了
}
setWidth();
console.log(width); // ReferenceError --- 全局找不到 width
从内存角度看变量生命周期
- 声明变量 = 在内存中申请一块区域
- 函数/代码块执行完毕后 → 垃圾回收,释放内存
- 作用域决定了变量的生命周期------该销毁的时候就销毁,不浪费内存
三、let vs const
let:可变的变量
js
let points = 50;
points = 51; // ✅ 值可以改
points = '52'; // ⚠️ 连类型都可以改,但不建议这么干
let 允许声明与赋值分离:
js
let a; // undefined,声明时不赋值也行
a = 100; // 稍后再赋值
const:不可变的常量
js
const key = 'abc123';
key = 'newKey'; // ❌ TypeError: Assignment to constant variable
const 声明时必须立即赋值:
js
const item; // ❌ SyntaxError: Missing initializer in const declaration
const 的"不可变"到底是什么意思?
这里有一个常见的误区------const 并不是让值完全不可变,而是让变量与值之间的绑定不可变。
对于简单数据类型(string、number、boolean 等),值本身不可改变:
js
const name = '张三';
name = '李四'; // ❌ TypeError
对于复杂数据类型(Object、Array 等),对象内部的属性可以修改:
js
const person = {
name: '王五',
age: 18
};
person.age++; // ✅ 可以改属性值
console.log(person); // { name: '王五', age: 19 }
person = '111'; // ❌ TypeError: Assignment to constant variable
// 不能把整个变量指向另一个值
记忆口诀:const 锁住的是"绑定",不是"内容"。简单数据类型的值等于绑定,所以改不了;复杂数据类型改里面可以,换整个对象不行。
四、经典面试题:for 循环 + setTimeout
这可能是面试中最高频的 var/let 考题了:
js
// 用 var
for (var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(`The number is ${i}`);
}, 1000);
}
// 输出:10 个 10
为什么全是 10?
var不支持块级作用域,整个循环只有一个ifor循环是同步代码,迅速执行完毕,i变成了 10- 1 秒后
setTimeout回调执行时,访问的是同一个i,值已经是 10 了
js
// 用 let
for (let i = 0; i < 10; i++) {
setTimeout(function() {
console.log(`The number is ${i}`);
}, 1000);
}
// 输出:0, 1, 2, 3, 4, 5, 6, 7, 8, 9
为什么用 let 就对了?
let支持块级作用域,每次循环迭代都会创建一个独立的作用域- 每一轮循环的
i都是一个新的变量,互不干扰 setTimeout回调捕获的是各自作用域中的i
这就是"闭包 + 块级作用域"的经典配合。
五、变量提升(Hoisting)深入理解
JS 代码的两阶段执行
一段 JavaScript 代码并不是写完就直接运行的,引擎会先"扫一遍",再"跑一遍": 
- 编译阶段:检查语法,创建执行上下文,处理变量声明(但不赋值)
- 执行阶段:逐行执行代码,进行赋值操作
编译阶段会创建执行上下文 ,其中包含两部分: 
- 变量环境(Variable Environment) :存放
var声明的变量和function函数声明,在编译阶段提前初始化 - 词法环境(Lexical Environment) :存放
let/const声明,编译阶段进入暂时性死区,不可访问 - 可执行代码:真正执行的语句,赋值操作在这里发生
var 的提升
js
console.log(pizza); // undefined
var pizza = 'Deep Dish';
编译阶段,引擎相当于做了这件事:
js
var pizza; // 提升声明,默认初始化为 undefined
console.log(pizza); // undefined
pizza = 'Deep Dish'; // 赋值留在原地
来看一个更完整的例子,感受变量提升的完整过程:
可以看到,编译阶段把 var myname 提升并初始化为 undefined,把 function showName 整体提升到顶部;执行阶段才真正给 myname 赋值 '极客时间'。
函数声明 vs 函数表达式的提升
这里有个容易混淆的细节------函数声明 和函数表达式的提升行为并不一样:

function foo(){}--- 完整提升:函数体整体被提升,声明前就可以调用var bar = function(){}--- 只提升变量声明 :bar被提升并初始化为undefined,函数体赋值留在原地,声明前调用会报错
let/const 的"暂时性死区"(TDZ)
js
console.log(pizza); // ReferenceError
let pizza = 'Deep Dish';
let 和 const 虽然也有提升(进入暂时性死区),但在声明语句执行之前不可访问 ,访问会抛出 ReferenceError。这更符合程序员的直觉。
小结
| 特性 | var | let | const |
|---|---|---|---|
| 变量提升 | ✅(初始化为 undefined) | ❌(暂时性死区) | ❌(暂时性死区) |
| 块级作用域 | ❌ | ✅ | ✅ |
| 函数作用域 | ✅ | ✅ | ✅ |
| 重复声明 | ✅ 允许 | ❌ 不允许 | ❌ 不允许 |
| 声明时赋值 | 可以不赋值 | 可以不赋值 | 必须赋值 |
| 重新赋值 | ✅ | ✅ | ❌(绑定不可变) |
六、最佳实践
- 默认使用
const:凡是不会重新赋值的变量,一律用const,语义更清晰,也防止意外修改 - 需要重新赋值用
let:比如循环计数器、累加器等场景 - 告别
var:ES6+ 时代,没有任何理由再用var
js
const name = '张三'; // 不变 → const
const config = { port: 3000 }; // 对象引用不变 → const
let count = 0; // 会变 → let
count++;
总结
var、let、const 三兄弟,本质上是 JavaScript 从一门"玩具语言"向"企业级语言"演进的缩影。var 是历史的遗留,let 和 const 是现代 JavaScript 的正确打开方式。
记住这三点就够了:
var--- 忘掉它let--- 会变的变量const--- 不变的绑定
理解了作用域和变量提升,你就真正掌握了 JavaScript 变量声明的底层逻辑。希望这篇文章能帮你彻底告别 var 带来的各种诡异 bug!
参考:ES6 标准规范、MDN Web Docs