JS 变量那些坑:从 var 到 let/const 的终极解密

583 阅读6分钟

JavaScript 深度解析varletconst变量声明

在 JavaScript 的学习过程中,变量声明是最基础却也是最容易踩坑的知识点。尤其是当你从传统的 Java/C++ 等语言背景过渡到 JavaScript 时,varletconst 的行为差异可能让你一头雾水。本文将结合示例和原理,带你深入理解 JavaScript 中变量的声明与作用域规则,并总结最佳实践。


1. 早期的 JavaScript 与 var

在 ES5(ECMAScript 5,即 2011 年标准)之前,JavaScript 只有 var 一种声明变量的方式。var 看似简单,但隐藏了很多坑。有些老师叫你直接弃用 var 转而使用 let ,但是并没有告诉你为什么,现在我来解释解释。

var age = 18; // js 是弱类型语言,变量的类型由值决定
age++;
var PI = 3.1415926; // 变量名大写通常表示常量
console.log(age); // 19

注意:虽然我们用大写 PI 表示“常量”,但是 var 并不能阻止你修改它。

PI = 3.14;
console.log(PI); // 3.14

这意味着,使用 var 时,变量既可以被重复声明,也可以被重新赋值,这在大型项目中很容易引起混乱。


1.1 var 的变量提升(Hoisting)

var 的最大特点之一就是变量提升。即便你在声明之前使用变量,代码也不会报 ReferenceError,而是返回 undefined

console.log(age); // undefined
var age = 18;

这里的执行顺序可以理解为:

  1. 编译阶段:JavaScript 引擎把所有 var 声明提前(提升)到函数或全局的最顶端,但不会赋值。
  2. 执行阶段:按原代码顺序执行,进行赋值。

这种机制虽然允许代码运行,但会降低代码可读性,容易导致 BUG。在现代开发中,建议尽量避免使用 var


1.2 var 的作用域问题

var函数作用域,不支持块级作用域。这意味着它在大括号 {} 内部声明时,依然属于外层函数或全局作用域:

{
    var age = 18;
}
console.log(age); // 18,仍然能访问

在复杂逻辑中,这种行为会让变量名冲突和状态管理变得非常麻烦。


2. ES6 引入的 letconst

从 ES6(2015 年)开始,JavaScript 引入了 letconst,提供了块级作用域和常量声明,从根本上解决了 var 带来的诸多问题。

let height = 188;
height++;
console.log(height); // 189

const key = 'abc123';
key = 'abc234'; // TypeError: Assignment to constant variable.

2.1 块级作用域

letconst 支持块级作用域,即变量只在 {} 内有效:

{
    let height = 188;
    const PI = 3.141592;
}

console.log(height); // ReferenceError
console.log(PI);     // ReferenceError

这与 var 的行为截然不同,能有效避免变量污染全局的问题。


2.2 暂时性死区(Temporal Dead Zone, TDZ)

letconst 有一个重要特点:暂时性死区。在变量声明之前访问,会报错 ReferenceError暂时性死区(Temporal Dead Zone, TDZ) 是指:

当你用 letconst 声明一个变量时,从代码块开始变量声明的位置之间的这段区域,变量处于一种“已声明但不可访问”的状态。如果在这段区域内访问变量,就会抛出 ReferenceError

换句话说:

  • 变量在编译阶段就已经知道存在,但在声明之前不能访问。
  • 直到变量真正执行声明(并赋值)之后,才能正常使用。
// 实例1.
const foo = () => {
    console.log('///')
    console.log(a)
    let a = 3;
}

foo()  //ReferenceError

或者

// 实例2.
const foo = () => {
    let a;
    console.log('///')
    console.log(a)
    a = 3;
}
foo();  //undefined

letconst可以说会变量提升但是被 TDZ 阻挡了访问,看起来就像没有提升

这种机制避免了 var 的变量提升带来的困扰,让代码更安全、更易读。


2.3 const 的使用注意点

const 声明的变量不能被重新赋值(简单数据类型):

简单数据类型包括:

  • Number
  • String
  • Boolean
  • undefined
  • null
  • Symbol(ES6 新增)
const PI = 3.1415926;
PI = 3.14; // TypeError

但是,const对象和数组是浅冻结的:

const person = { name: "gg", age: 20 };
person.age = 21; // 可以修改对象属性
console.log(person); // { name: "gg", age: 21 }

const wes = Object.freeze(person); // 深冻结对象
wes.age = 17; // 无效
console.log(wes); // { name: "gg", age: 21 }

简单类型(number、string、boolean)的 const 完全不可变,而复杂类型(对象、数组)引用地址不可变,但属性可变。用 Object.freeze 可以实现深层不可变。


3. 函数与变量提升的差异

在 JavaScript 中,函数也是一等公民,可以像变量一样被提升,但提升行为与 var 不完全相同。

setWidth(); // 运行正常

function setWidth() {
    var width = 100;
    console.log(width); // 100
}

// console.log(width); // ReferenceError

特点总结:

  • 函数声明会连同函数体一起提升。
  • var 声明的变量只会提升声明,不会提升赋值。
  • 函数内部使用 varletconst,遵循各自的作用域规则。

因此,函数提升可以在调用前使用函数,而 var 变量提升只能得到 undefined


4. 总结 varletconst

特性varletconst
作用域函数作用域块级作用域块级作用域
变量提升是(赋值未提升)是,但有 TDZ是,但有 TDZ
可重复声明
可修改值否(对象属性可改,但引用不可改)

4.1 开发实践建议

  1. 不再使用 var
    let + const 结合使用,保证代码安全和可读性。
  2. 默认使用 const
    只有在需要重新赋值时才使用 let
  3. 对象或数组尽量用 Object.freeze
    保证数据不可变,避免副作用。
  4. 理解 TDZ 与作用域
    有助于减少 ReferenceError,让调试更简单。

5. 常见错误集合

  • ReferenceError: height is not defined
    访问未声明的变量。
  • TypeError: Assignment to constant variable.
    const 变量重新赋值。
  • ReferenceError: Cannot access 'PI' before initialization
    暂时性死区内访问 letconst 变量。

理解这些错误,能够帮助你快速定位问题,并加深对作用域和提升机制的理解。


6. 代码示例总结

6.1 块级作用域

块级作用域只在大括号 {} 内有效,并且主要影响 letconst

  • 普通块 {}
  • if / else
  • switch case
  • try / catch
  • for / while 本身不是块级作用域,它们只是语句。
  • var 只遵循函数作用域(或全局作用域),忽略块级作用域
{
    var age = 18;    // 不支持块级作用域或忽略块级作用域
    let height = 188;  // 支持块级作用域
}
console.log(age);    // 18
console.log(height); // ReferenceError

6.2 对象常量

const person = { name: "gg", age: 20 };
person.age = 21; // 可修改属性
console.log(person);

const wes = Object.freeze(person);
wes.age = 17; // 无效
console.log(wes);

6.3 函数提升

setWidth(); // 可以调用

function setWidth() {
    var width = 100;
    console.log(width); // 100
}
// console.log(width); // ReferenceError

7. 总结

  1. var 适合老旧代码维护,现代开发不推荐使用。
  2. let 提供块级作用域,解决 var 的变量提升问题。
  3. const 用于不可变值或引用,结合 Object.freeze 可实现深冻结。
  4. 理解 TDZ 和提升机制,能够避免常见的 ReferenceErrorTypeError
  5. 在企业级开发中,合理使用 letconst 能显著提升代码质量和可维护性。