上一篇说的是JS的关键字与保留字,其中提到了var、let、const这三个关键字——它们是JS中声明变量、常量的核心工具。
不知道什么时候用var、什么时候用let,更分不清const和let的区别,随手用var声明变量,可能会遇到变量泄漏、重复声明覆盖等问题,只要吃透三者的核心区别、作用域和使用场景,就能彻底避开这些坑。
阅读收获:彻底分清var、let、const的核心区别(作用域、变量提升、重复声明等),掌握三者的使用场景,能根据需求灵活选择合适的声明方式,写出规范、可维护的JS代码。
1. 为什么有三种变量声明方式?
既然都是声明变量,为什么JS要搞var、let、const三种方式?其实这和JS的版本更新有关,核心是为了规范代码、避免bug。
var:是JS早期版本(ES5及之前)唯一的变量声明方式,存在很多设计缺陷,比如变量泄漏、允许重复声明,现在已被行业淘汰,不推荐使用;let、const:ES6新增的声明方式,修复了var的所有缺陷,增加了块级作用域、暂时性死区等特性,是当前主流、推荐使用的声明方式;
核心原则:优先使用const(值不修改时),其次用let(值需要修改时),彻底放弃var(避免踩坑)。
虽然var现在还能在部分旧项目中看到,新手最好还是不要使用var,直接学习let和const的规范用法,避免养成不良编码习惯,减少后续修改成本。
2. var、let、const 核心区别
ECMAScript 变量是松散类型的,意思是变量可以保存任何类型的数据。每个变量都是一个用来保存任意值的命名占位符。
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函数作用域 / 全局作用域(无块级作用域) | 块级作用域({}界定,如if、for块内) | 块级作用域({}界定,与let一致) |
| 变量提升 | 存在(声明提升到顶部,值为undefined) | 不存在(暂时性死区,声明前访问报错) | 不存在(暂时性死区,声明前访问报错) |
| 重复声明 | 允许(同一作用域可重复声明) | 不允许(同一作用域不可重复声明) | 不允许(同一作用域不可重复声明) |
| 重新赋值 | 允许(可随意修改值,甚至改变类型) | 允许(值可修改,最好不要改变类型) | 必须初始化,不可重新赋值(对象/数组可修改内部属性) |
3. var声明(已淘汰)
var是JS早期的变量声明方式,存在很多设计缺陷,现在主流项目已完全不用。核心记住:var 无块级作用域、存在变量提升、允许重复声明,这三个特性即可。
3.1 var 声明的核心特征
- 无块级作用域:if、for等块内声明的变量,块外可访问(变量泄漏)
if (true) {
var name = "橘朵"; // 块内用var声明
console.log(name); // 输出:橘朵(块内正常访问)
}
console.log(name); // 输出:橘朵(块外也能访问,变量泄漏,不符合直觉)
- 存在变量提升:声明被提升到作用域顶部,赋值留在原地,声明前访问值为undefined
console.log(age); // 输出:undefined(不报错,变量提升)
var age = 25; // 声明+赋值
//ECMAScript 运行时把它看成等价于如下代码
var age;
console.log(age); // undefined
age=25;
- 允许重复声明:同一作用域可多次声明同名变量,后面的覆盖前面的
var num = 10;
var num = 20; // 重复声明,不报错
console.log(num); // 输出:20(后面的覆盖前面的)
- 可随意重新赋值,甚至改变类型(不推荐,易导致逻辑错误)
var message = "你好";
message = 100; // 改变类型,不报错
console.log(message); // 输出:100
3.2 var声明在实际开发中可能遇到的问题
在实际开发中,var的变量提升特性会带来一些意想不到的问题。下面是几个典型的场景:
3.2.1 循环中的变量泄漏
在 for循环中使用 var声明计数器,循环结束后这个 变量仍然存在于循环体所在的作用域中,这可能导致错误引用。
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 输出 3, 3, 3
}, 100);
}
console.log("循环结束后的 i:", i); // 输出 3
问题原因:变量 i被提升到全局或函数作用域,整个循环中共享同一个变量。异步操作(如 setTimeout)触发时,循环已结束,i的值为 3。
解决方案:使用 let声明循环变量,它为每次迭代创建新的绑定。每个回调函数捕获的是当次循环的 j值,而且循环外部无法访问 j。
for (let j = 0; j < 3; j++) {
setTimeout(function() {
console.log(j); // 输出 0, 1, 2
}, 100);
}
console . log (j); // 报错:j is not defined
3.2.2 条件块内的意外覆盖
由于 var是函数级作用域,在条件块(如 if)内部声明的变量会覆盖外部同名变量。
function example() {
var x = 1;
if (true) {
var x = 2; // 覆盖了外层的 x
console.log(x); // 输出 2
}
console.log(x); // 输出 2,而非预期的 1
}
example();
问题原因:if块内的 var x被提升到函数顶部,与函数开头声明的 x是同一个变量,导致内部赋值影响外部。
解决方案:使用 let或 const声明块级作用域变量。if块内的 x与外层的 x互不干扰。
function exampleFixed() {
let x = 1;
if (true) {
let x = 2; // 独立的块级变量
console.log(x); // 输出 2
}
console.log(x); // 输出 1
}
3.2.3 变量使用先于声明
变量提升允许在声明前使用变量,但值为 undefined,这可能导致逻辑错误。
console.log(username); // 输出 undefined
var username = "Alice";
问题原因:var username的声明被提升,但赋值 (= "Alice") 仍在原地。在声明前访问,得到 undefined。
解决方案:使用 let或 const,它们存在“暂时性死区”(TDZ),在声明前访问会抛出错误,有助于提前发现问题。
console . log (usernameLet); // 报错:Cannot access 'usernameLet' before initialization
let usernameLet = "Alice";
3.2.4 函数与变量声明的优先级
当同一个作用域下存在同名函数声明和 var变量声明时,它们的提升优先级可能导致意外。
console.log(typeof foo); // 输出 "function"
var foo = "variable";
function foo() {
console.log("I'm a function");
}
console.log(typeof foo); // 输出 "string"
问题原因:函数声明提升优先于变量声明提升。最初 foo是函数,但后来被变量赋值覆盖为字符串。
解决方案:避免在同一作用域内重复声明同名标识符。使用 let或 const声明变量可避免此问题,因为它们不允许重复声明。
function bar () {}
let bar = "variable" ; // 报错:Identifier 'bar' has already been declared
4. let声明 (推荐使用,变量首选)
let是ES6新增的变量声明方式,跟var的作用差不多,但修复了var的所有缺陷,核心特性:块级作用域、无变量提升(暂时性死区)、不允许重复声明、可重新赋值,适合声明“值需要修改”的。
和var最明显的区别是,let 声明的范围是块作用域,而 var 声明的范围是函数作用域。
let 声明的变量只在其所在的代码块(例如 {} 、 if 语句、 for 循环)内有效。 这能避免变量泄露到外部,尤其是循环或条件判断。
4.1 let 的核心用法
- 块级作用域:块内声明的变量,块外不可访问(无变量泄漏)
if (true) {
let age = 25;
console.log(age); // 输出:25(块内正常访问)
}
// console.log(age); // 报错:age is not defined(块外不可访问,符合预期)
- 无变量提升(暂时性死区):声明前访问报错,强制先声明后使用
// console.log(gender); // 报错:Cannot access 'gender' before initialization
let gender = "女";
- 不允许重复声明:同一作用域内,同名变量不能重复声明
let userName = "橘朵";
// let userName = "张三"; // 报错:Identifier 'userName' has already been declared
- 可重新赋值:值可以修改,建议不改变类型(提升代码可读性)
let score = 85;
score = 90; // 允许重新赋值
// score = "优秀"; // 不推荐,改变类型,易导致逻辑错误
console.log(score); // 输出:90
- 嵌套声明:不同作用域内,可声明同名变量(互不干扰)
let color = "红色";
if (true) {
let color = "蓝色"; // 独立的块级变量
console.log(color); // 输出:蓝色
}
console.log(color); // 输出:红色
4.2 let 的特殊注意点
- 注意1:
let不能和var、const在同一作用域内重复声明(即使声明方式不同,也会报错); - 注意2:
let在全局作用域声明的变量,不会成为window对象的属性(与var不同); - 注意3:不建议用
let进行条件声明(比如在if块内声明let变量,块外无法访问,容易导致逻辑错误)。
// 错误:同一作用域,let与var重复声明
var name = "Matt";
// let name = "橘朵"; // 报错:Identifier 'name' has already been declared
// 区别:let在全局声明,不成为window属性
var name = "Matt";
console.log(window.name); // 输出:Matt(var声明的全局变量,挂载到window)
let age = 25;
console.log(window.age); // 输出:undefined(let声明的全局变量,不挂载到window)
//不建议用let进行条件声明,条件外无法访问,可以在块外先声明,或使用三元运算符
let someVar; // 先声明
// ... 一些逻辑
if (someCondition) {
someVar = 'value A';
} else {
someVar = 'value B';
}
// 使用三元运算符
let someVar = someCondition ? 'value A' : 'value B' ;
5. const声明 (有限推荐,常量首选)
const 也是ES6新增的声明方式,和let基本相同,唯一一个重要的区别是用它声明的变量必须初始化,而且不可以重新赋值,修改 const 声明的变量会导致运行时错误。
关键点注意:const声明是“引用不可变”,不是“值不可变”。如果const 声明的时候对象/数组,可以修改对象内部的属性、数组内的元素。
5.1 const的核心用法
- 必须初始化:声明时必须赋值,否则报错
const MY_CONSTANT; // SyntaxError: Missing initializer in const declaration
const MY_CONSTANT = "CONSTANT";
- 不可以重新赋值:赋值后不能修改,否则报错
const age = 26;
//age = 36; // TypeError: 给常量赋值
- 块级作用域:和
let一致,块内声明,块外不可访问
if (true) {
const message = "Hello";
console.log(message); // 输出:Hello
}
// console.log(message); // 报错:message is not defined
const不允许重复声明:和let一致,同一作用域不可以重复声明
const name = 'Matt';
const name = 'Nicholas'; // SyntaxError
- 对象/数组可以修改内部的属性/元素(引用不变即可)
// 声明对象
const user = { name: "橘朵", age: 25 };
user.age = 26; // 允许,修改对象内部属性(不改变引用)
console.log(user.age); // 输出:26
// user = { name: "张三" }; // 报错:Assignment to constant variable.(改变引用)
// 声明数组
const fruits = ["苹果", "香蕉"];
fruits.push("橙子"); // 允许,修改数组内部元素(不改变引用)
console.log(fruits); // 输出:["苹果", "香蕉", "橙子"]
// fruits = ["葡萄", "西瓜"]; // 报错:Assignment to constant variable.(改变引用)
5.2 const的使用场景
const的使用场景很广泛,只要变量的值不需要修改,都优先用const,能避免意外的修改,提升代码可读性和安全性:
- 场景1:声明固定常量(如PI、最大/最小值、固定配置)
const PI = 3.14159;
const MAX_UPLOAD_SIZE = 10 * 1024 * 1024;
- 场景2:声明对象、数组(只要不重新赋值,就用
const)
const user = { name: "橘朵" };
const fruits = ["苹果", "香蕉"];
- 声明函数(如箭头函数、普通函数,不重新赋值时)
const add = (a, b) => a + b;
- 场景4:
for-of、for-in循环中,声明不需要修改的迭代变量
// for-of循环
const arr = [1, 2, 3];
for (const item of arr) {
console.log(item); // 输出:1, 2, 3(item不需要修改,用const)
}
// for-in循环
for (const key in {a: 1, b: 2}) {
console.log(key) ; // a, b
}
6. var、let、const 使用规范
- 规范1:彻底放弃
var,无论什么场景,都不用var声明变量,避免变量泄漏、重复声明等问题; - 规范2:优先使用
const,只要变量的值不需要修改(包括对象、数组),就用const,能避免意外修改; - 规范3:按需使用
let,只有当变量的值需要动态修改(比如循环计数器、动态赋值的变量),才用let; - 规范4:避免重复声明、改变变量类型,同一作用域内不重复声明同名变量,变量赋值时尽量不改变类型(提升代码可读性)。
7. 总结
这篇讲的是var、let、const的核心区别和用法,核心就是记住:放弃var,优先用const,按需用let。记住使用规范,就能规范声明变量,写出更安全、可维护的JS代码。
总结一下核心要点:
var:无块级作用域、有变量提升、允许重复声明,已淘汰,不推荐使用;let:块级作用域、无变量提升(暂时性死区)、不允许重复声明、可重新赋值,适合值需要修改的变量;const:块级作用域、无变量提升(暂时性死区)、不允许重复声明、必须初始化、不可重新赋值,适合值不修改的常量(对象/数组可修改内部属性);- 核心规范:放弃
var,优先const,按需let,不重复声明、不改变变量类型。
下一篇预告,解锁JS中最基础的“类型检测工具”——typeof操作符,判断数据类型。