前言 :JavaScript 诞生于 1995 年,Brendan Eich 只花了一周就完成了最初的实现。作为浏览器的"副产品",JS 在匆忙之中留下了不少设计瑕疵------
var关键字就是其中最典型的一个。2015 年 ES6 发布,let和const正式登场,补齐了 JS 在作用域和常量声明方面的短板。本文从var_let_const目录下的实验代码出发,系统梳理三者的区别、作用域规则、变量提升机制,以及从 ES5 迁移到 ES6+ 的实践。
目录
- [JavaScript 与 ES6:一次迟到的补课](#JavaScript 与 ES6:一次迟到的补课 "#javascript-%E4%B8%8E-es6%E4%B8%80%E6%AC%A1%E8%BF%9F%E5%88%B0%E7%9A%84%E8%A1%A5%E8%AF%BE")
- [声明变量并赋值:var vs let vs const](#声明变量并赋值:var vs let vs const "#%E5%A3%B0%E6%98%8E%E5%8F%98%E9%87%8F%E5%B9%B6%E8%B5%8B%E5%80%BCvar-vs-let-vs-const")
- 作用域:变量住在哪里?
- [var 的块级作用域缺陷](#var 的块级作用域缺陷 "#var-%E7%9A%84%E5%9D%97%E7%BA%A7%E4%BD%9C%E7%94%A8%E5%9F%9F%E7%BC%BA%E9%99%B7")
- [for + setTimeout:一道经典面试题](#for + setTimeout:一道经典面试题 "#for--settimeout%E4%B8%80%E9%81%93%E7%BB%8F%E5%85%B8%E9%9D%A2%E8%AF%95%E9%A2%98")
- [const 的"不可变"到底指什么?](#const 的"不可变"到底指什么? "#const-%E7%9A%84%E4%B8%8D%E5%8F%AF%E5%8F%98%E5%88%B0%E5%BA%95%E6%8C%87%E4%BB%80%E4%B9%88")
- 变量提升:先编译,再执行
- 总结与实践
JavaScript 与 ES6:一次迟到的补课
JavaScript 诞生之初,目标很简单------给网页添加一点交互能力(幻灯片、表单校验等)。它是一门弱类型、动态类型的脚本语言,值决定类型,变量只是容器的标签。
随着 Web 从"展示页"变成"应用平台",企业级大型项目对 JS 提出了更高的要求。2015 年发布的 ES6(ECMAScript 2015) 是 JS 历史上最大的一次版本升级,引入的 let 和 const 直接改变了开发者声明变量的方式。
| 时代 | 声明方式 | 特点 |
|---|---|---|
| ES5 及以前 | var |
只有函数作用域,没有块级作用域,存在变量提升 |
| ES6+ | let / const |
支持块级作用域,let 无变量提升,const 声明常量 |
ES5 时代没有真正的常量机制,开发者只能靠命名约定来"假装"有常量------
var PI = 3.1415926,var CHATMODEL = 'deepseek-chat'。全大写的变量名是在告诉同事"别改我",但语言层面没有任何约束。ES6 的const终于把这个约定变成了规则。
声明变量并赋值:var vs let vs const
三种声明方式的语法差异,从 3.js 中可以一目了然:
javascript
// const:声明时必须赋值
// const item; // ❌ 报错:Missing initializer in const declaration
const item = 1; // ✅ 声明 + 赋值一步到位
// let:声明和赋值可以分开
let a; // ✅ 先声明,值为 undefined
a = 100; // ✅ 后赋值
// var:ES5 的旧方式,不推荐
var height = 200; // 能跑,但现在不该用
| 关键字 | 声明时赋值 | 可重新赋值 | 可重新声明 | 块级作用域 |
|---|---|---|---|---|
var |
不必须 | ✅ | ✅ | ❌ |
let |
不必须 | ✅ | ❌ | ✅ |
const |
必须 | ❌ | ❌ | ✅ |
核心原则就一条:默认用 const,需要改值用 let,永远别用 var。
作用域:变量住在哪里?
作用域决定了变量的"可见范围"。JavaScript 有三种作用域层级,从 1.js 可以清晰看到它们的嵌套关系:
三种作用域对比
| 作用域类型 | 边界 | 适用声明 | 示例 |
|---|---|---|---|
| 全局作用域 | 整个脚本 | 尽量少用 | var height = 200 |
| 函数局部作用域 | function 的花括号内 |
var / let / const |
function setWidth() { var width = 100; } |
| 块级作用域 | 任意 { } 内(if/for/裸花括号) |
仅 let / const |
{ const name = "张三"; } |
变量查找规则:冒泡查找
1.js 中的例子完美演示了这一点:
javascript
var height = 200; // 全局作用域
function setWidth() {
var width = 100; // 函数局部作用域
console.log(width, height);// ✅ 100 200 ------ width 在当前作用域找到,height 冒泡到全局找到
}
setWidth();
// console.log(width); // ❌ ReferenceError ------ width 在函数内,外面看不见
当函数执行完毕,其内部的局部变量会被垃圾回收 。从内存角度看:声明变量 = 申请一块内存区域,函数销毁 = 回收那块内存。这就是变量的生命周期。
var 的块级作用域缺陷
这是 var 最大的设计问题------它不认 { } 代码块的边界。看 1.js 中的 if 语句:
javascript
var age = 100;
if (age > 12) {
// 这里是一个块级作用域
var dog = age * 7; // ❌ var 把 dog 泄露到了外层
let x = 111; // ✅ let 把 x 关在了块里
console.log(dog); // 700
}
console.log(dog); // 700 ------ var 声明的 dog 跑出来了!
console.log(x); // ❌ ReferenceError: x is not defined ------ let 守住了边界
2.js 进一步验证------用一个裸花括号创建的代码块:
javascript
{
const name = "张三";
console.log(name); // ✅ "张三"
}
// console.log(name); // ❌ ReferenceError ------ 退出代码块,变量已被回收
设计背景 :JS 是浏览器大战时期的仓促产物,设计时并未考虑大型应用的复杂性。
var的块级作用域缺失不是"错误",而是 JavaScript 1.0 时代根本没有"块级作用域"这个需求------当时一个脚本文件也就几十行代码。今天任何一个前端项目都可能包含成百上千个模块,没有块级作用域的var已经变成了绕不开的坑。
for + setTimeout:一道经典面试题
var 和 let 在 for 循环中的行为差异是一道高频面试题。2.js 给出了教科书级的演示:
javascript
// 使用 var ------ 不想要的结果
for (var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(`This number is ${i}`);
}, 1000);
}
// 输出:10 个 "This number is 10"
// 使用 let ------ 想要的结果
for (let i = 0; i < 10; i++) {
setTimeout(function() {
console.log(`This number is ${i}`);
}, 1000);
}
// 输出:This number is 0, 1, 2, ... 9
为什么 var 打印的全是 10?
var 不支持块级作用域,整个 for 循环只有一个 i。同步代码(循环本身)执行得很快,i 从 0 跑到了 10 退出循环。1 秒后 setTimeout 回调触发时,它们读到的都是同一个已经是 10 的 i。
为什么 let 能正确打印?
let 支持块级作用域,每次循环迭代都会创建一个独立的作用域,各自保存各自的 i 值。所以 10 个 setTimeout 回调各自读到自己闭包里的 i。
这道题考察的不只是
var和let的语法区别,更核心的是对**作用域 + 事件循环(同步/异步)**的理解。
const 的"不可变"到底指什么?
const 全称是 constant variable (常量变量),这个名字本身就透露出它的双重性格。从 3.js 的实验中可以梳理出完整的规则:
简单数据类型:值不可变
javascript
const key = 'abc123';
// key = 'ABC123'; // ❌ TypeError: Assignment to constant variable
let points = 50;
points = 51; // ✅ let 可以改值
points = '52'; // ⚠️ let 甚至可以改类型(不要这么做!)
复杂数据类型:值可变,类型(引用)不可变
javascript
const person = {
name: '李',
age: 18
};
person.age++; // ✅ 可以修改对象属性
console.log(person); // { name: '李', age: 19 }
// person = '111'; // ❌ TypeError: Assignment to constant variable
核心区分:
- 简单数据类型 (string、number、boolean 等):
const让值本身不可变 - 复杂数据类型 (object、array):
const锁定的是引用地址,对象的内部属性仍可修改
打个比方:const 是一根定海神针------针的位置不能动,但绑在针上的东西可以换。
变量提升:先编译,再执行
JavaScript 的执行分为两个阶段,4.js 揭示了 var 和 let 在这两个阶段中的行为差异:
javascript
// var 的变量提升
console.log(pizza); // undefined ------ 变量提升了,但值还没赋
var pizza = 'Deep Dish';
// let 没有变量提升
console.log(pizza); // ❌ ReferenceError: Cannot access 'pizza' before initialization
let pizza = 'Deep Dish';
三种错误信息的含义
在调试过程中,你可能会遇到这三种错误,它们各自指向不同类型的问题:
| 错误信息 | 含义 | 典型场景 |
|---|---|---|
Assignment to constant variable |
试图给 const 变量重新赋值 |
const x = 1; x = 2; |
ReferenceError: xxx is not defined |
变量从未声明(冒泡到全局也没找到) | 直接用未声明的变量名 |
ReferenceError: Cannot access 'pizza' before initialization |
let/const 的暂时性死区 |
声明前使用 let/const 变量 |
变量提升是一个"不应存在"的设计缺陷------它与代码的书写顺序和直觉相悖。好在
let和const直接废掉了这个机制:用let声明的变量,在声明之前的那段"暂时性死区"(TDZ)中无法访问。只要你坚持先声明、后使用的原则,就不会踩坑。
总结与实践
从 var 到 let/const,JavaScript 的变量声明体系完成了一次从"能用"到"好用"的跨越:
三条核心规则
- 默认用
const------只要不需要重新赋值,就用const。它最安全、意图最清晰。 - 需要改值用
let------循环计数器、累加器、状态变量,用let。 - 永远别用
var------除非你在维护上古代码。var的块级作用域缺失和变量提升是颗定时炸弹。
声明方式速查表
| 场景 | 用哪个 | 示例 |
|---|---|---|
| 不变的值 / 配置常量 | const |
const API_URL = 'https://...' |
| 对象引用不变 | const |
const user = { name: '李' } |
| 循环计数器 | let |
for (let i = 0; i < n; i++) |
| 需要重新赋值的变量 | let |
let result = 0; result += n; |
| 任何情况 | ❌ var |
2026 年了,别再用了 |
最终建议:JS 的变量声明并不复杂,关键是理解背后的作用域 和执行阶段 两个概念。打开控制台,把
var_let_const目录下的四个 JS 文件跑一遍,亲眼看看var泄露到块外、let在 setTimeout 里保留闭包值、const锁定引用但不锁定属性------当你看到每个 console.log 的输出和预期一致时,这套知识就真正内化了。