告别 var,拥抱 let 和 const:JavaScript 变量声明完全指南

前言 :JavaScript 诞生于 1995 年,Brendan Eich 只花了一周就完成了最初的实现。作为浏览器的"副产品",JS 在匆忙之中留下了不少设计瑕疵------var 关键字就是其中最典型的一个。2015 年 ES6 发布,letconst 正式登场,补齐了 JS 在作用域和常量声明方面的短板。本文从 var_let_const 目录下的实验代码出发,系统梳理三者的区别、作用域规则、变量提升机制,以及从 ES5 迁移到 ES6+ 的实践。


目录

  1. [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")
  2. [声明变量并赋值: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")
  3. 作用域:变量住在哪里?
  4. [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")
  5. [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")
  6. [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")
  7. 变量提升:先编译,再执行
  8. 总结与实践

JavaScript 与 ES6:一次迟到的补课

JavaScript 诞生之初,目标很简单------给网页添加一点交互能力(幻灯片、表单校验等)。它是一门弱类型、动态类型的脚本语言,值决定类型,变量只是容器的标签。

随着 Web 从"展示页"变成"应用平台",企业级大型项目对 JS 提出了更高的要求。2015 年发布的 ES6(ECMAScript 2015) 是 JS 历史上最大的一次版本升级,引入的 letconst 直接改变了开发者声明变量的方式。

时代 声明方式 特点
ES5 及以前 var 只有函数作用域,没有块级作用域,存在变量提升
ES6+ let / const 支持块级作用域,let 无变量提升,const 声明常量

ES5 时代没有真正的常量机制,开发者只能靠命名约定来"假装"有常量------var PI = 3.1415926var 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 可以清晰看到它们的嵌套关系:

flowchart TD A["全局作用域<br/>var height = 200"] --> B["函数局部作用域<br/>function setWidth() { ... }"] B --> C["块级作用域<br/>if (age > 12) { ... }"] A --> C

三种作用域对比

作用域类型 边界 适用声明 示例
全局作用域 整个脚本 尽量少用 var height = 200
函数局部作用域 function 的花括号内 var / let / const function setWidth() { var width = 100; }
块级作用域 任意 { } 内(if/for/裸花括号) let / const { const name = "张三"; }

变量查找规则:冒泡查找

flowchart LR A["当前作用域查找"] -->|"找到了"| B["✅ 使用该变量"] A -->|"没找到"| C["向外层作用域查找"] C -->|"找到了"| B C -->|"没找到"| D["继续向外冒泡..."] D -->|"到全局都没找到"| E["❌ ReferenceError: xxx is not defined"]

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 守住了边界
flowchart TD subgraph "if 代码块 { }" A["var dog = age * 7"] B["let x = 111"] end A -->|"var 无视块边界"| C["外层作用域能访问 dog ✅"] B -->|"let 尊重块边界"| D["外层作用域访问 x ❌<br/>ReferenceError"]

2.js 进一步验证------用一个裸花括号创建的代码块:

javascript 复制代码
{
    const name = "张三";
    console.log(name);        // ✅ "张三"
}
// console.log(name);         // ❌ ReferenceError ------ 退出代码块,变量已被回收

设计背景 :JS 是浏览器大战时期的仓促产物,设计时并未考虑大型应用的复杂性。var 的块级作用域缺失不是"错误",而是 JavaScript 1.0 时代根本没有"块级作用域"这个需求------当时一个脚本文件也就几十行代码。今天任何一个前端项目都可能包含成百上千个模块,没有块级作用域的 var 已经变成了绕不开的坑。


for + setTimeout:一道经典面试题

varlet 在 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?

flowchart TD A["var i = 0 → i 是全局(或函数)作用域的<br/>整个循环共享同一个 i"] --> B["i 从 0 递增到 9 → 循环结束"] B --> C["i = 10 → 条件 i < 10 不满足,退出循环"] C --> D["1 秒后,10 个 setTimeout 回调执行"] D --> E["所有回调读取的都是同一个 i → 10"]

var 不支持块级作用域,整个 for 循环只有一个 i。同步代码(循环本身)执行得很快,i 从 0 跑到了 10 退出循环。1 秒后 setTimeout 回调触发时,它们读到的都是同一个已经是 10 的 i

为什么 let 能正确打印?

flowchart TD A["let i = 0 → 每次迭代创建一个<br/>独立的块级作用域"] --> B["迭代 0: 块级 i=0,setTimeout 捕获 i=0"] A --> C["迭代 1: 块级 i=1,setTimeout 捕获 i=1"] A --> D["..."] A --> E["迭代 9: 块级 i=9,setTimeout 捕获 i=9"]

let 支持块级作用域,每次循环迭代都会创建一个独立的作用域,各自保存各自的 i 值。所以 10 个 setTimeout 回调各自读到自己闭包里的 i

这道题考察的不只是 varlet 的语法区别,更核心的是对**作用域 + 事件循环(同步/异步)**的理解。


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
flowchart TD subgraph "const person = { name: '李', age: 18 }" A["栈内存<br/>person → 引用地址 0x001"] B["堆内存<br/>0x001: { name: '李', age: 18 }"] end A -->|"const 锁定的是这个引用<br/>不能指向别的地址"| A B -->|"对象的属性不在此约束范围内<br/>可以自由修改"| B

核心区分:

  • 简单数据类型 (string、number、boolean 等):const 让值本身不可变
  • 复杂数据类型 (object、array):const 锁定的是引用地址,对象的内部属性仍可修改

打个比方:const 是一根定海神针------针的位置不能动,但绑在针上的东西可以换。


变量提升:先编译,再执行

JavaScript 的执行分为两个阶段,4.js 揭示了 varlet 在这两个阶段中的行为差异:

javascript 复制代码
// var 的变量提升
console.log(pizza);       // undefined ------ 变量提升了,但值还没赋
var pizza = 'Deep Dish';

// let 没有变量提升
console.log(pizza);       // ❌ ReferenceError: Cannot access 'pizza' before initialization
let pizza = 'Deep Dish';
flowchart TD subgraph "编译阶段" A["扫描代码,建立执行上下文"] B["var pizza → 在变量环境中注册,初始化为 undefined"] C["let pizza → 注册但保持 uninitialized 状态<br/>(暂时性死区 TDZ)"] end subgraph "执行阶段" D["逐行执行代码"] E["var: 读到 var pizza = 'Deep Dish' 才赋值"] F["let: 声明前访问 → ReferenceError"] end A --> B A --> C B --> D C --> D D --> E D --> F

三种错误信息的含义

在调试过程中,你可能会遇到这三种错误,它们各自指向不同类型的问题:

错误信息 含义 典型场景
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 变量

变量提升是一个"不应存在"的设计缺陷------它与代码的书写顺序和直觉相悖。好在 letconst 直接废掉了这个机制:用 let 声明的变量,在声明之前的那段"暂时性死区"(TDZ)中无法访问。只要你坚持先声明、后使用的原则,就不会踩坑。


总结与实践

varlet/const,JavaScript 的变量声明体系完成了一次从"能用"到"好用"的跨越:

graph TD subgraph "ES5 时代的困境" A["var 没有块级作用域"] B["没有真正的常量"] C["变量提升打乱直觉顺序"] end subgraph "ES6+ 的解法" D["let / const<br/>支持块级作用域"] E["const<br/>真正的不可变绑定"] F["let / const<br/>不支持变量提升"] end A --> D B --> E C --> F

三条核心规则

  1. 默认用 const ------只要不需要重新赋值,就用 const。它最安全、意图最清晰。
  2. 需要改值用 let ------循环计数器、累加器、状态变量,用 let
  3. 永远别用 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 的输出和预期一致时,这套知识就真正内化了。


相关推荐
如果超人不会飞12 小时前
别再自己套壳了!三分钟把你的浏览器变成 AI 的“提线木偶”——WebMCP 深度解析
javascript
烛衔溟12 小时前
TypeScript 高级类型与工具类型全解
javascript·ubuntu·typescript
之歆13 小时前
Day22_CSS 函数完全指南:从变量到数学计算的现代样式编程
开发语言·前端·javascript·css·tensorflow·less
ZengLiangYi13 小时前
Prompt 工程:让 LLM 输出结构化 JSON
前端·javascript·后端
米丘13 小时前
React19.x 一个示例来看 Diff 算法
javascript·react.js
zithern_juejin13 小时前
手写instanceof
javascript
ZengLiangYi13 小时前
MCP 协议从零实现:手写最简 MCP Server
前端·javascript·后端
yspwf13 小时前
Node.js 本地下载并使用 Hugging Face 中文向量模型:以 bge-base-zh-v1.5 为例
javascript·后端
小救星小杜、13 小时前
new Router base的作用
前端·javascript·vue.js