彻底搞懂JavaScript块级作用域与函数作用域:var、let、const的核心区别
在JavaScript中,作用域是控制变量访问权限的核心机制,直接影响代码的安全性、可维护性甚至运行结果。尤其是ES6引入块级作用域后,var、let、const 三者的作用域差异成为前端面试和日常开发的高频考点。从基础概念出发,彻底理清函数作用域与块级作用域的区别,以及不同声明关键字的用法边界。
一、先搞懂:什么是函数作用域?(ES5及之前的核心作用域)
在ES6之前,JavaScript中只有两种作用域:全局作用域和函数作用域。其中函数作用域是核心,它的核心规则是:var 声明的变量,作用域被限制在包含它的函数内部,在整个函数体内都有效,不受内部代码块(如if、for 花括号)的限制。
1.1 函数作用域的直观示例
function foo() {
if (true) {
var x = 10; // 用var声明变量
}
console.log(x); // 10,正常输出
}
foo();
console.log(x); // 报错:x is not defined
从示例能看出:
- 变量
x虽然在if代码块内声明,但作用域覆盖整个foo函数,所以在if外部、函数内部依然能访问。 - 函数外部无法访问
x,因为var被函数作用域限制,超出函数则失去访问权限。
1.2 函数作用域的“坑”:变量提升
函数作用域的另一个典型特征是 var 声明的变量会发生“变量提升”——变量声明被提升到函数或全局作用域的顶部,且默认初始化为 undefined,这常常导致意外的bug。
function test() {
console.log(x); // undefined(变量提升导致此处不报错)
if (true) {
var x = 5; // 声明被提升到函数顶部,赋值保留在原地
}
}
test();
如果不理解变量提升,很容易误以为 console.log(x) 会报错,但实际上 var x 已经被提升到函数顶部,只是尚未赋值,所以输出 undefined。
二、ES6新特性:块级作用域的诞生
ES6(ECMAScript 2015)为了解决 var 带来的变量污染、作用域模糊等问题,正式引入了块级作用域。其核心规则是:变量的作用域被限制在最近的一对花括号 {} 内(如if、for、while 代码块或独立花括号块),外部无法访问。
实现块级作用域的核心关键字就是:let 和 const。
2.1 块级作用域的直观示例
// 独立代码块
{
let a = 10;
const b = 20;
console.log(a); // 10,块内正常访问
console.log(b); // 20,块内正常访问
}
console.log(a); // 报错:a is not defined
console.log(b); // 报错:b is not defined
// if代码块
if (true) {
let y = 20;
}
console.log(y); // 报错:y is not defined
从示例能清晰看出:let、const 声明的变量,严格被限制在花括号 {} 内部,超出这个块就失去访问权限,这就是块级作用域的核心特性。
2.2 为什么说var没有块级作用域?
这是 var 与 let/const 的核心区别之一:var声明的变量不会被花括号 {} 限制,只会被函数作用域或全局作用域限制。用一个对比示例更直观理解:
if (true) {
var a = 1; // var声明,不受块级限制
let b = 2; // let声明,受块级限制
}
console.log(a); // 1,正常输出(全局作用域可访问)
console.log(b); // 报错:b is not defined
如果没有函数包裹,var a 的作用域就是全局,即使在 if 块内声明,外部依然能访问;而 let b 被块级作用域限制,外部无法访问。
三、核心对比:var vs let/const 全维度差异
除了作用域差异,var 与 let/const 在变量提升、重复声明等方面也有显著区别,整理成表格一目了然:
| 特性 | var | let / const |
|---|---|---|
| 作用域 | 函数作用域 或 全局作用域 | 块级作用域(花括号{}限制) |
| 变量提升 | 有,声明提升到作用域顶部,初始化为 undefined | 有声明提升,但不初始化(进入暂时性死区,访问报错) |
| 重复声明 | 允许(同一作用域内可重复声明同一变量) | 不允许(同一作用域内重复声明会报错) |
| 常量限制 | 无(声明后可任意修改值) | const 声明的变量不可修改(引用类型仅限制引用地址),let 可修改 |
关键补充:暂时性死区(TDZ)
对于 let/const 的“声明提升但不初始化”,需要特别理解暂时性死区:从作用域开始到变量声明语句之前,该变量处于“死区”,任何访问都会报错。
if (true) {
console.log(y); // 报错:Cannot access 'y' before initialization
let y = 20; // 声明语句之前为暂时性死区
}
四、块级作用域的实际应用:解决哪些问题?
块级作用域的引入,不仅规范了变量作用域,还解决了很多ES5时代的“坑”,最典型的有两个场景:
4.1 避免变量污染全局/外层作用域
在循环、条件判断等代码块中使用 let/const,可以避免变量泄漏到外层作用域,减少变量冲突。
// 反面示例:var导致全局污染
for (var i = 0; i < 3; i++) {
// 循环逻辑
}
console.log(i); // 3(i泄漏到全局作用域)
// 正面示例:let限制块级作用域
for (let j = 0; j < 3; j++) {
// 循环逻辑
}
console.log(j); // 报错:j is not defined(无泄漏)
4.2 解决循环中的闭包问题
ES5中用var 声明循环变量,由于没有块级作用域,容易导致闭包访问到错误的变量值;而 let 每次迭代都会创建新的作用域,完美解决这个问题。
// 反面示例:var导致闭包访问错误
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 输出:3 3 3
}
// 正面示例:let解决闭包问题
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 输出:0 1 2
}
原因:var i 是函数/全局作用域,循环中只创建一个变量,定时器回调访问的都是同一个最终值 3;而 let i 每次迭代都创建新的块级作用域,每个定时器回调访问的是当前迭代的 i。
五、总结:开发中该如何选择?
理解了作用域的核心差异后,日常开发的选择原则很简单:
- 优先使用
const:大多数变量声明后不需要修改,用const能明确语义,避免意外修改,提高代码安全性。 - 需要修改的变量用
let:如循环变量、需要重新赋值的变量,用let限制块级作用域,避免污染。 - 坚决不用
var:ES6之后,var的所有功能都能被let/const替代,且var的作用域和变量提升特性容易引发bug,没必要再使用。
最后再梳理核心要点:块级作用域由{} 定义,通过 let/const 实现;var 只有函数作用域,无块级限制;合理使用块级作用域能避免变量污染和提升代码可维护性。