看到这个标题点进来的,多半是在var、let、const之间纠结过的前端新人。JavaScript变量声明这个事,说小很小,三句话就能把语法列完;说大也真大,项目里一半以上的隐性bug、面试里一半以上的基础题,都能绕回作用域和声明方式上。尤其最近不少同学在群里聊前端面试题时提到var和let的区别、javascript:void(0)这类运行时报错,还有一堆看着像是变量声明引起的控制台报错,我觉得有必要把这套东西从头到尾捋一遍。
这篇不会只给你列语法,我会把变量提升、暂时性死区、块级作用域、闭包陷阱这些“为什么”讲透,再配合实际项目中真正会用到的场景、高频报错和排查思路,最后附上几个适合新人自查的题目。不管你是刚开始接触前端,还是已经在写业务代码但偶尔被变量问题坑到,这篇都值得花十分钟看完,能帮你省下后面很多排查时间。
1. 变量声明的前世今生:为什么一个声明能写出三套语法
1.1 三种声明方式的基本定位
JavaScript里的变量声明经历了两个阶段:ES5时代只有var,到了ES6(也就是ES2015)正式引入了let和const。很多新人第一次看到这个会觉得“是不是多此一举”,实际上不是,let和const的出现是为了解决var在作用域和变量提升上埋下的各种坑。
先说最基础的语法差异。var声明一个变量,你可以反复对同一个名字声明多次,后面一次会覆盖前面一次,不会报错。let和const则不允许在同一个作用域里重复声明同一个变量,一旦重复,控制台会直接抛出Uncaught SyntaxError: Identifier 'x' has already been declared。
javascript复制var a = 1;
var a = 2; // 完全没问题,a 变成 2
let b = 1;
let b = 2; // 报错:Uncaught SyntaxError: Identifier 'b' has already been declared
这个差异看起来简单,但背后反映的是设计思路的变化:var比较宽松,写起来随意,但也正因为随意,很容易在复杂项目里不小心覆盖掉别人的变量或者自己之前定义的变量。let和const则是一种更“克制”的设计——同一个作用域里,名字就是唯一的,强迫你在命名时想清楚。
const和let的区别只有一个字面层面的差异:const声明时必须赋值,而且声明后不能再对这个变量本身重新赋值。注意“变量本身”这个说法,后面我会专门讲const对象到底能不能改的问题,这里先记住最基础的一条——const并不是让你声明一个完全不可变的值。
1.2 从一份报错说起:变量提升与暂时性死区
新手最容易在控制台看到这样的现象:用var声明的变量,在声明语句之前就能访问,值还是undefined,不报错。很多人第一次遇到时一脸懵,以为代码写错了,其实这里涉及的是“变量提升”(hoisting)。
变量提升指的是:var声明的变量,在代码执行之前会被“提升”到当前作用域的顶部。不过提升的只是声明,不是赋值。所以代码执行到声明语句之前时,变量已经存在了,但值还是undefined。
javascript复制console.log(a); // undefined,不报错
var a = 10;
上面这段代码实际执行时等价于:
javascript复制var a;
console.log(a); // undefined
a = 10;
这就是为什么很多老前辈会说“var的声明被提升了”。听着很玄,其实可以理解成一个“先占坑后填值”的过程:变量名先出现在作用域里,赋值操作原地不动。
let和const本质上也会被提升,但和var不一样的是:它们在声明语句执行之前,会处在一个“暂时性死区”(Temporal Dead Zone,简称TDZ)。在这个阶段访问变量,会直接抛错,而不是返回undefined。
javascript复制console.log(b); // Uncaught ReferenceError: Cannot access 'b' before initialization
let b = 10;
为什么var是undefined而let是报错?关键差异在于:var在提升时会被初始化为undefined,而let和const在提升时不会初始化,进入暂时性死区。强调一点:暂时性死区从作用域开始就生效,一直到声明语句执行完才结束。哪怕这个变量是全局变量,在声明前访问同样会报错。
面试题里经常出现一道经典题目:
javascript复制var a = 10;
if (true) {
console.log(a); // 这里会输出什么?
let a = 20;
}
很多人认为会输出10,实际结果是直接报错。原因就是块级作用域内的let a让这个块从一开始就进入了暂时性死区,所以在声明前访问a,不会去外层找那个var a,而是直接抛Cannot access 'a' before initialization。这个坑在真实代码里很容易踩到,尤其是你把某个变量从var改造成let时,如果有一行代码放在声明语句前面,本来悄悄拿到undefined的程序会瞬间崩溃。从某种角度看,这其实是好事——让问题暴露得更早,而不是留到后面产生诡异结果。
1.3 一张表理清三者的核心差异
为了让你快速建立全局认知,我把三者的核心差异整理成一张表,建议保存下来当备忘。
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
| 变量提升 | 提升,且初始化为 undefined | 提升,但不初始化(TDZ) | 提升,但不初始化(TDZ) |
