# 一文搞懂 var、let、const —— ES6 变量声明完全指南

2 阅读4分钟

前言

JavaScript 的变量声明经历了从 varlet/const 的演进,这背后是 ES6 对语言设计的重大修正。本文结合代码示例,系统梳理三者差异、作用域、变量提升等核心概念,适合复习和面试准备。


一、JavaScript 与 ES6 的背景

先说点历史。JavaScript 和 Java 没有任何关系,纯粹是当年蹭热度起的名。JS 是 Brendan Eich 用一周时间赶工出来的浏览器副产品,最初只用来给网页加点交互(表单验证、幻灯片之类)。因为是 KPI 项目,设计上难免有瑕疵——var 就是其中之一。

ES6(ECMAScript 2015)是 JS 的一次断档式升级,标志着 JS 向企业级大型项目开发迈进。letconst 的引入,就是用来修复 var 的历史遗留问题。


二、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

var 只有全局作用域函数作用域{} 代码块对它形同虚设。let/const 则支持块级作用域{} 内的变量外面拿不到。

// 2.js
{
    const name = '张三';
}
console.log(name); // ReferenceError —— name 被锁在块内

三、经典面试题:for + setTimeout

这是考察作用域理解的高频题:

// 用 var —— 翻车版
for (var i = 0; i < 10; i++) {
    setTimeout(function () {
        console.log(`This number is ${i}`);
    }, 1000);
}
// 输出:10 个 "This number is 10"

为什么? var 不支持块级作用域,整个循环共享同一个 i。同步代码先跑完,i 变成 10,1 秒后所有 setTimeout 回调拿到的都是同一个 i

// 用 let —— 正确版
for (let i = 0; i < 10; i++) {
    setTimeout(function () {
        console.log(`This number is ${i}`);
    }, 1000);
}
// 输出:0 1 2 3 4 5 6 7 8 9

为什么? let 支持块级作用域,每次迭代都会创建一个独立的块级作用域,保存当时的 i 值。10 次迭代 = 10 个独立的 i

一句话:var 是"大家共用一个",let 是"各用各的"。


四、const:不可变变量constant variable(常量)

// 3.js
const key = 'abc123';
key = 'newKey'; // ❌ Assignment to constant variable

// const 声明时必须赋值
const a; // ❌ Missing initializer
let b;   // ✅ let 可以先声明后赋值

基本类型 vs 引用类型

const 的"不可变"针对的是内存地址

// 基本类型:值直接存栈,不能改
const point = 50;
point = 1;      // ❌ 报错

// 引用类型:变量存的是堆地址,地址不变就行,内容可以改
const person = {
    name: '野原新之助',
    age: 5
};
person.age++;          // ✅ 可以修改属性
person = { name: '风间' }; // ❌ 不能更换整个对象(地址变了)
person = '111';        // ❌ 不能改变数据类型

记忆技巧

  • 基本数据类型 → 值和类型都不能改
  • 引用数据类型 → 可以改内容,不能换类型/换对象

五、作用域与变量查找规则

JS 有三种作用域:

作用域说明对应声明
全局作用域代码最外层所有
函数作用域function 内部var
块级作用域{} 内部letconst

变量查找规则:作用域链

当前作用域 → 外层作用域 → ... → 全局作用域 → 报错

从内向外冒泡查找,找到就停,全局都找不到就 ReferenceError

垃圾回收:函数/代码块执行完后,内部变量会被销毁、内存被回收。这就是变量的生命周期


六、变量提升(Hoisting)与暂时性死区

// 4.js
console.log(pizza);                // ❌ ReferenceError: Cannot access 'pizza' before initialization
let pizza = 'Deep Dish';

JS 执行分两阶段:

阶段做什么
编译阶段检测语法,准备执行上下文,变量声明被"提起"
执行阶段逐行执行代码

var 的提升:

console.log(x); // undefined(不是报错!因为 var 被提升了,默认值是 undefined)
var x = 10;

let/const 虽然有提升,但进入暂时性死区(TDZ),在声明之前访问会报错:

console.log(x); // ReferenceError
let x = 10;

let/const 不是为了消除提升,而是让该报错的时候报错——这比 varundefined 更符合直觉,更容易排查 bug。


七、一表总结

特性varletconst
作用域全局 / 函数块级 {}块级 {}
重复声明
变量提升✅ 初始化为 undefined✅ 但 TDZ 报错✅ 但 TDZ 报错
声明时赋值可选可选必须
可重新赋值
修改属性(对象)

八、最佳实践

  1. 默认用 const,只有确定要重新赋值才用 let
  2. 彻底告别 var,ES6+ 代码里没有它存在的理由
  3. 声明放在作用域顶部,避免 TDZ 报错
  4. for 循环用 let,远离 var + setTimeout 的坑

本文基于个人学习笔记整理,参考示例代码见ai_doubao_zzh: 走向AGI,走向豆包