《被骂了20年的 var 终于退休了:ES6 块级作用域是如何拯救 JavaScript 的?》

8 阅读4分钟

ES6 核心特性完全指南:从变量声明到作用域解析

告别 var 的坑,拥抱 let 和 const 的现代 JavaScript 开发

前言

1995年,Brendan Eich 只用了10天就设计出了 JavaScript。这个“一周 KPI 项目”如今已成为全球最流行的编程语言之一。早期设计的一些“瑕疵”一直困扰着开发者,直到 ES6(ECMAScript 2015)的出现,JavaScript 才真正迈入了企业级开发的时代。

今天,我们就来深入理解 ES6 中最重要的变量声明和作用域机制。

一、从 var 到 let/const 的进化

1.1 var 的历史问题

在 ES5 时代,我们只能用 var 声明变量:

var name = "JavaScript";
var PI = 3.1415926;
var CHAT_MODEL = "deepseek-chat";

var 的主要问题:不支持块级作用域

1.2 let 和 const 的优势

ES6 引入了两个新的声明方式:

  • let:声明变量,值可以改变
  • const:声明常量,值不可改变(针对简单数据类型)
// let 的使用
let points = 50;
points = 51;      // ✅ 可以重新赋值
points = "51";    // ⚠️ 可以改变类型(但不推荐)

// const 的使用
const KEY = 'abc123';
KEY = 'ABC123';   // ❌ TypeError: Assignment to constant variable

// const 必须在声明时赋值
const GENDER;     // ❌ SyntaxError: Missing initializer

1.3 复杂数据类型与 const

对于对象和数组等复杂数据类型,const 只保证引用不变,内部属性可以修改:

const person = {
    name: '马牛逼',
    age: 18
};

person.age++;     // ✅ 可以修改属性
console.log(person); // { name: '马牛逼', age: 19 }

person = "111";   // ❌ 不能重新赋值

二、作用域深度解析

2.1 三种作用域类型

// 1. 全局作用域
var globalVar = "我在任何地方都能访问";

// 2. 函数作用域
function setWidth() {
    var width = 100;  // 仅在函数内可访问
    console.log(width);
}

// 3. 块级作用域(ES6 新增)
if (true) {
    let blockVar = "我只在块内存在";
    const blockConst = "我也是块级作用域";
}
console.log(blockVar); // ❌ ReferenceError

2.2 作用域嵌套与变量查找

变量查找遵循作用域链规则:从当前作用域向上冒泡查找,直到全局作用域。

var height = 100;  // 全局变量

function setWidth() {
    var width = 100;  // 局部变量
    console.log(width, height); // 100, 100
}

setWidth();
console.log(width);  // ❌ ReferenceError: width is not defined

三、经典坑位:for 循环中的 setTimeout

这是面试中出现频率最高的问题之一:

// 使用 var(错误示例)
for (var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(`This number is ${i}`); // 打印10个10
    }, 1000);
}
// 输出:10 10 10 10 10 10 10 10 10 10

为什么? var 不支持块级作用域,所有的异步回调共享同一个 i,循环结束后 i 变成了 10。

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

为什么有效? let 每次循环都会创建一个新的块级作用域,每个回调都绑定着自己那一轮的 i 值。

四、变量提升(Hoisting)

4.1 var 的提升行为

JavaScript 代码在执行前会经过编译阶段,变量声明会被提升到作用域顶部。

console.log(pizza);  // undefined(不是报错!)
var pizza = 'Deep Dish';
console.log(pizza);  // 'Deep Dish'

// 上面的代码实际执行顺序:
var pizza;           // 声明提升
console.log(pizza);  // undefined
pizza = 'Deep Dish'; // 赋值留在原地
console.log(pizza);  // 'Deep Dish'

4.2 let/const 的临时死区(TDZ)

letconst 虽然也会提升,但存在临时死区(Temporal Dead Zone):

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

在声明之前访问变量会报错,这避免了 var 带来的混乱。

五、最佳实践建议

5.1 优先级原则

  1. 默认使用 const:除非变量需要重新赋值
  2. 需要重新赋值时使用 let:如计数器、累加器
  3. 永远不要使用 var:除非维护遗留代码
// ✅ 好的做法
const API_URL = 'https://api.example.com';
let count = 0;
let isLoading = false;

// ❌ 不好的做法
var oldStyle = '避免使用';

5.2 代码示例对比

// 不好的写法
var winner = false;
winner = '戴';  // 类型随意改变,难以维护

// 好的写法
let winner = false;
winner = true;  // 保持布尔类型

// 更好的写法
const WINNER = '戴';  // 如果值不会改变

六、常见错误速查表

错误信息含义解决方案
Assignment to constant variable尝试修改 const 声明的变量改用 let,或确认是否需要修改
ReferenceError: x is not defined变量从未声明先声明再使用
Cannot access 'x' before initialization在声明前访问 let/const 变量将变量声明移到使用之前

结语

ES6 的 letconst 不仅解决了 var 的历史遗留问题,还让 JavaScript 的作用域规则更加清晰、可预测。掌握这些基础知识,是写出健壮、可维护代码的第一步。

记住:默认用 const,需要改的用 let,忘记 var