JavaScript 变量
变量是"装数据的盒子"。JS 里有三种声明变量的关键字:let、const、var。它们看起来差不多,行为却有天壤之别。这一章我们彻底搞清楚它们的区别,避免日后写出一堆莫名其妙的 bug。
1. 三种声明方式
JS 历史上只有 var 一种声明方式,坑特别多。ES6 引入了 let 和 const,基本替代了 var。
// 现代 JS 三种声明方式
let age = 20; // 变量:可以重新赋值
const name = "小明"; // 常量:声明时必须赋值,不能再改
var old = 1; // 老语法,有坑,尽量别用
// 规则:能用 const 就用 const,会变的才用 let
const PI = 3.14;
let count = 0;
count = 1; // ✅ let 可以重新赋值
// PI = 3.1415; // ❌ const 不能重新赋值,会报错使用原则:
- 能
const就const(默认 80% 的变量都不需要改)。 - 会重新赋值的用
let(计数器、循环变量、累加器)。 - 永远不要用
var(除非维护老代码)。
2. 块级作用域 vs 函数作用域
作用域就是变量"在哪个范围内能被访问"。let/const 是块级作用域:只在最近的 {} 内有效;而 var 是函数作用域:在整个函数内都有效,无视 {}。
// let / const 是"块级作用域",只在 {} 内有效
{
let x = 1;
const y = 2;
console.log(x, y); // 1 2
}
// console.log(x); // ❌ ReferenceError,x 在外面不存在
// for 循环里的 let
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出: 0 1 2(let 每轮循环都是新的 i)
// 对比 var(函数作用域,有变量提升)
for (var j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 100);
}
// 输出: 3 3 3(三秒后 j 已经是 3)上面这个 for 循环的例子是 let 和 var 最经典的对比——let 每轮循环都会创建新的 i,所以闭包能记住不同的值;var 共享同一个 j,等 setTimeout 回调执行时,j 早就是 3 了。
3. 变量提升与暂时性死区
JS 引擎在执行代码前会先扫描所有声明,把它们"提升"到作用域顶部。但 let/const 和 var 的提升行为不同:
var:声明提升,值为undefined,访问不报错。let/const:声明也提升,但进入暂时性死区(TDZ),访问会直接报错。
// ❌ 在声明之前访问会报错(暂时性死区)
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 1;
// ✅ var 有变量提升,声明被提到顶部,但值是 undefined
console.log(y); // undefined(不报错,但容易出 bug)
var y = 2;
// 实际等价于:
// var y; // 提升,值为 undefined
// console.log(y); // undefined
// y = 2; // 赋值留在原地
// 函数声明也会提升,可以在定义前调用
greet(); // ✅ "你好"
function greet() { console.log("你好"); }暂时性死区存在的目的是避免你误用还没初始化的变量。最佳实践是在用到变量的地方就近声明,不要依赖提升。
4. const 的真相:并非"完全不可变"
很多人以为 const 声明的就是常量、永远不会变——这是误解。const 只是禁止"重新赋值整个变量",但如果存的是对象或数组,它的内部是可以改的:
// const 声明的"常量"只是不能重新赋值
// 但如果存的是对象/数组,内部仍可修改!
const user = { name: "小明", age: 20 };
user.age = 21; // ✅ 可以,改的是对象的属性
user.city = "北京"; // ✅ 可以,加新属性
console.log(user); // { name: "小明", age: 21, city: "北京" }
// ❌ 不能重新赋值整个变量
// user = { name: "小红" }; // TypeError
// 想让对象完全不可变,用 Object.freeze()
const config = Object.freeze({ host: "localhost" });
config.host = "1.2.3.4"; // 严格模式报错,普通模式静默失败
console.log(config.host); // 仍然是 "localhost"记住这条规则:const 锁的是"绑定",不是"值"。如果想完全锁死,用 Object.freeze()。但要注意 freeze 只能冻一层,深层属性仍可改。
5. var 的那些坑(老代码才会遇到)
虽然新项目不用 var,但你维护老代码时一定会遇到。看完下面这些坑,你会更庆幸有 let/const 可用:
// var 的几个坑,看完你就知道为啥不用它
// 1. 重复声明不报错(容易覆盖已有变量)
var x = 1;
var x = 2; // ✅ 不报错(let 会报错)
// 2. 变量提升,值是 undefined
console.log(a); // undefined
var a = 1;
// 3. 没有块级作用域,泄漏到外面
if (true) {
var secret = 123;
}
console.log(secret); // 123(let/const 会报错)
// 4. 全局变量成为 window 属性
var globalVar = 1;
console.log(window.globalVar); // 1(let 不会)这四个坑任意一个都够你调半天。记住:新项目一律用 let/const,看到老代码里的 var 可以放心地改成 let(除非确认它依赖某个特殊行为)。
6. 全局变量 vs 局部变量
// 函数内用 let/const 声明的是局部变量,外部访问不到
function test() {
let local = 1;
const alsoLocal = 2;
}
// console.log(local); // ReferenceError
// 函数内不写 let/const 直接赋值 = 隐式全局(严格模式报错)
function bad() {
leaked = 1; // 普通模式:变成 window.leaked
}
bad();
console.log(window.leaked); // 1
// 全局声明(模块顶层)
let globalLet = 1;
const globalConst = 2;隐式全局是 JS 最坑的设计之一,严格模式和 ES6 模块都会报错阻止它。新项目务必启用严格模式。
7. 同时声明多个变量
// 一行声明多个
let a = 1, b = 2, c = 3;
// 也可以解构批量赋值
const [x, y, z] = [10, 20, 30];
const { name, age } = { name: "小明", age: 20 };
console.log(name, age); // 小明 20小结
记住这三句话就够了:
- const 优先,let 次之,不用 var。
- let/const 是块级作用域,var 是函数作用域。
- const 不让重新赋值,但对象内部可改。
下一篇我们看 JS 的数据类型——数字、字符串、布尔,以及怎么判断一个值是什么类型。
← 上一篇 JavaScript 基础语法
下一篇 JavaScript 数据类型 →