一文搞懂 JavaScript 变量声明:var、let、const 到底有什么区别?

一文搞懂 JavaScript 变量声明:var、let、const 到底有什么区别?

前言

JavaScript 是一门弱类型的动态语言,诞生于网景(Netscape)时代,最初的设计目标很简单------给网页加点交互、做做幻灯片、操作一下 DOM。据说 Brendan Eich 只花了一周就写出了 JS 的雏形,妥妥的"KPI 项目"。

因为是赶工出来的,JS 早期设计上有很多不完美的地方,变量声明 就是其中之一。直到 ES6(ECMAScript 2015)发布,letconst 的出现才真正解决了 var 遗留下来的诸多坑。

今天就让我们从作用域、变量提升、循环陷阱等角度,一次性把 varletconst 的区别讲透。


一、var 时代的问题

在 ES6 之前,声明变量只能用 var,连常量的概念都没有,全靠程序员的代码规范来约束:

js 复制代码
var PI = 3.1415926; // 希望大家不要改它,但它其实可以改...

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 --- let 则不会

var 声明的变量无视 {} 代码块,直接穿透到外层。这在大型项目中极易造成变量污染。

2. 存在变量提升(Hoisting)

js 复制代码
console.log(pizza);          // undefined --- 不报错?!
var pizza = 'Deep Dish';

换成 let

js 复制代码
console.log(pizza);          // ReferenceError: Cannot access 'pizza' before initialization
let pizza = 'Deep Dish';

这就是所谓的变量提升 ------var 声明的变量在编译阶段就被提升到作用域顶部并初始化为 undefined,导致代码行为与直觉不符。letconst 则不存在这种诡异的提升行为。


二、作用域(Scope)

理解作用域是掌握变量声明的关键。

三种作用域

作用域类型 说明 var let const
全局作用域 代码最外层
函数作用域 function 内部
块级作用域 { } 代码块内部

一句话总结:var 只认函数,不认花括号;letconst 函数和花括号都认。

作用域嵌套与变量查找

JS 的变量查找遵循冒泡规则

  1. 先在当前作用域中查找
  2. 找到了 → 直接用
  3. 没找到 → 向外层作用域冒泡查找
  4. 一直找到全局作用域还没找到 → 停下,报 ReferenceError
js 复制代码
var height = 200; // 全局作用域

function setWidth() {
  var width = 100; // 函数局部作用域
  console.log(width); // 100 --- 在当前作用域找到了
  console.log(height); // 200 --- 当前没找到,冒泡到全局找到了
}

setWidth();
console.log(width); // ReferenceError --- 全局找不到 width

从内存角度看变量生命周期

  • 声明变量 = 在内存中申请一块区域
  • 函数/代码块执行完毕后 → 垃圾回收,释放内存
  • 作用域决定了变量的生命周期------该销毁的时候就销毁,不浪费内存

三、let vs const

let:可变的变量

js 复制代码
let points = 50;
points = 51;       // ✅ 值可以改
points = '52';     // ⚠️ 连类型都可以改,但不建议这么干

let 允许声明与赋值分离:

js 复制代码
let a;    // undefined,声明时不赋值也行
a = 100;  // 稍后再赋值

const:不可变的常量

js 复制代码
const key = 'abc123';
key = 'newKey'; // ❌ TypeError: Assignment to constant variable

const 声明时必须立即赋值:

js 复制代码
const item; // ❌ SyntaxError: Missing initializer in const declaration

const 的"不可变"到底是什么意思?

这里有一个常见的误区------const 并不是让值完全不可变,而是让变量与值之间的绑定不可变

对于简单数据类型(string、number、boolean 等),值本身不可改变:

js 复制代码
const name = '张三';
name = '李四'; // ❌ TypeError

对于复杂数据类型(Object、Array 等),对象内部的属性可以修改:

js 复制代码
const person = {
  name: '王五',
  age: 18
};

person.age++;           // ✅ 可以改属性值
console.log(person);    // { name: '王五', age: 19 }

person = '111';         // ❌ TypeError: Assignment to constant variable
                        // 不能把整个变量指向另一个值

记忆口诀:const 锁住的是"绑定",不是"内容"。简单数据类型的值等于绑定,所以改不了;复杂数据类型改里面可以,换整个对象不行。


四、经典面试题:for 循环 + setTimeout

这可能是面试中最高频的 var/let 考题了:

js 复制代码
// 用 var
for (var i = 0; i < 10; i++) {
  setTimeout(function() {
    console.log(`The number is ${i}`);
  }, 1000);
}
// 输出:10 个 10

为什么全是 10?

  • var 不支持块级作用域,整个循环只有一个 i
  • for 循环是同步代码,迅速执行完毕,i 变成了 10
  • 1 秒后 setTimeout 回调执行时,访问的是同一个 i,值已经是 10 了
js 复制代码
// 用 let
for (let i = 0; i < 10; i++) {
  setTimeout(function() {
    console.log(`The number is ${i}`);
  }, 1000);
}
// 输出:0, 1, 2, 3, 4, 5, 6, 7, 8, 9

为什么用 let 就对了?

  • let 支持块级作用域,每次循环迭代都会创建一个独立的作用域
  • 每一轮循环的 i 都是一个新的变量,互不干扰
  • setTimeout 回调捕获的是各自作用域中的 i

这就是"闭包 + 块级作用域"的经典配合。


五、变量提升(Hoisting)深入理解

JS 代码的两阶段执行

一段 JavaScript 代码并不是写完就直接运行的,引擎会先"扫一遍",再"跑一遍":

  1. 编译阶段:检查语法,创建执行上下文,处理变量声明(但不赋值)
  2. 执行阶段:逐行执行代码,进行赋值操作

编译阶段会创建执行上下文 ,其中包含两部分:

  • 变量环境(Variable Environment) :存放 var 声明的变量和 function 函数声明,在编译阶段提前初始化
  • 词法环境(Lexical Environment) :存放 let/const 声明,编译阶段进入暂时性死区,不可访问
  • 可执行代码:真正执行的语句,赋值操作在这里发生

var 的提升

js 复制代码
console.log(pizza);          // undefined
var pizza = 'Deep Dish';

编译阶段,引擎相当于做了这件事:

js 复制代码
var pizza;                   // 提升声明,默认初始化为 undefined
console.log(pizza);          // undefined
pizza = 'Deep Dish';         // 赋值留在原地

来看一个更完整的例子,感受变量提升的完整过程: 可以看到,编译阶段把 var myname 提升并初始化为 undefined,把 function showName 整体提升到顶部;执行阶段才真正给 myname 赋值 '极客时间'

函数声明 vs 函数表达式的提升

这里有个容易混淆的细节------函数声明函数表达式的提升行为并不一样:

  • function foo(){} --- 完整提升:函数体整体被提升,声明前就可以调用
  • var bar = function(){} --- 只提升变量声明bar 被提升并初始化为 undefined,函数体赋值留在原地,声明前调用会报错

let/const 的"暂时性死区"(TDZ)

js 复制代码
console.log(pizza);          // ReferenceError
let pizza = 'Deep Dish';

letconst 虽然也有提升(进入暂时性死区),但在声明语句执行之前不可访问 ,访问会抛出 ReferenceError。这更符合程序员的直觉。

小结

特性 var let const
变量提升 ✅(初始化为 undefined) ❌(暂时性死区) ❌(暂时性死区)
块级作用域
函数作用域
重复声明 ✅ 允许 ❌ 不允许 ❌ 不允许
声明时赋值 可以不赋值 可以不赋值 必须赋值
重新赋值 ❌(绑定不可变)

六、最佳实践

  1. 默认使用 const :凡是不会重新赋值的变量,一律用 const,语义更清晰,也防止意外修改
  2. 需要重新赋值用 let:比如循环计数器、累加器等场景
  3. 告别 var :ES6+ 时代,没有任何理由再用 var
js 复制代码
const name = '张三';        // 不变 → const
const config = { port: 3000 }; // 对象引用不变 → const
let count = 0;              // 会变 → let
count++;

总结

varletconst 三兄弟,本质上是 JavaScript 从一门"玩具语言"向"企业级语言"演进的缩影。var 是历史的遗留,letconst 是现代 JavaScript 的正确打开方式。

记住这三点就够了:

  • var --- 忘掉它
  • let --- 会变的变量
  • const --- 不变的绑定

理解了作用域和变量提升,你就真正掌握了 JavaScript 变量声明的底层逻辑。希望这篇文章能帮你彻底告别 var 带来的各种诡异 bug!


参考:ES6 标准规范、MDN Web Docs

相关推荐
槑有老呆1 小时前
解密 JS 变量提升:告别玄学,读懂 V8 编译与代码执行逻辑
javascript
无糖可可果1 小时前
拆穿 JavaScript 变量提升的"魔术"——从一段反直觉代码说起
javascript
问心无愧05131 小时前
ctf show web入门261
android·前端·笔记
月光刺眼1 小时前
🎶二分 · 双指针 · 滑动窗口 · 螺旋矩阵:数组算法四题拆解
javascript·算法
触底反弹1 小时前
你真的理解 JavaScript 变量提升(Hoisting)吗?从 V8 引擎编译原理深入剖析
前端·面试
蜡台1 小时前
Vue2 使用 typescript 教程
前端·vue.js·typescript
光影少年2 小时前
Redux Toolkit 用法、解决原生Redux 冗余问题
开发语言·前端·javascript·react.js·中间件·前端框架·ecmascript
云水一下2 小时前
JavaScript 从零基础到精通系列:DOM 操作与事件驱动编程
前端·javascript
零陵上将军_xdr2 小时前
后端转全栈学习-Day3-JavaScript 基础-1
开发语言·javascript·学习