广告位 · 页头(后台-模块管理-新建模块后替换此区域)

JavaScript变量声明方式有哪些?作用域规则如何理解?

JavaScript 中声明变量主要有三种方式:var、let 和 const。它们不只是语法上的差别,更直接决定了变量在代码中的可见范围、生命周期以及是否可以被重新赋值。很多难以排查的 bug,比如循环中异步回调拿到同一个值、全局对象被意外修改、函数外访问块内变量,追根溯源都是作用域规则没有理解清楚。下面先系统梳理 var、let、const 的声明特性,再分析作用域链和变量提升,最后给出实际项目中的选择建议。

JavaScript变量声明方式有哪些?作用域规则如何理解?

var 的声明特性与函数作用域

var 是 JavaScript 最早提供的变量声明关键字,它的作用域单位是函数,而不是代码块。也就是说,在 if、for、while 等语句块中用 var 声明的变量,并不会被限制在这个花括号里面。只要这个块位于某个函数内部,变量就会成为该函数的局部变量;如果不在函数内部,则会成为全局变量。这个特性经常导致变量穿透块边界,给代码维护带来麻烦。

例如下面这段代码,很多人会以为 result 只在 if 块中有效,但实际在块外依然可以访问:

function testVar() {
  if (true) {
    var result = 'done';
  }
  console.log(result); // done
}
testVar();

另外 var 还允许重复声明同名变量。在同一个函数作用域内写两个 var name 不会报错,后一个声明会覆盖前一个。这在多人协作或代码合并时很容易造成变量值被悄悄改写。还有一个副作用是,在浏览器全局环境下,使用 var 声明的全局变量会挂载到 window 对象上,而 let 和 const 不会。这意味着 var 可能覆盖 window 上已有的属性,引发全局污染。

let 与 const 的块级作用域

let 和 const 从 ES6 开始引入,它们的核心改进就是块级作用域。所谓块级作用域,指的是任何一对花括号 { } 包裹的区域,包括 if 语句、for 循环、while 循环以及单独使用花括号创建的作用域。在块内用 let 或 const 声明的变量,只能在当前块及其嵌套块中访问,一旦离开这个花括号范围,变量就无法再使用。

这种机制非常适合循环计数器。传统的 var 写法会让循环变量泄漏到循环外,而 let 可以让计数器生命周期和循环绑定:

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 输出 0 1 2

const 与 let 的作用域规则完全一致,区别只在于 const 声明的变量必须在声明时初始化,并且不能重新赋值。这里的不可重新赋值针对的是变量指向的引用,而不是引用类型本身的内容。如果用 const 声明一个对象或数组,仍然可以修改它的属性或元素:

const user = { name: 'Ada' };
user.name = 'Grace'; // 允许
user = {}; // 报错 TypeError: Assignment to constant variable.

由于块级作用域的存在,let 和 const 不能在同一作用域内重复声明同名变量,否则会抛出语法错误。这在一定程度上避免了变量覆盖问题。

作用域链与闭包

当 JavaScript 引擎需要查找一个变量时,会从当前执行的作用域开始,逐层向外查找,直到全局作用域。这层由内向外排列的变量对象就构成作用域链。内部作用域可以访问外部作用域的变量,反过来则不行。如果内层声明了同名变量,会遮蔽外层变量,导致外层同名变量在内层不可见。

作用域链是理解闭包的基础。闭包是指函数在定义时会保存对外层词法作用域的引用,即使这个函数在外部作用域执行完毕后才被调用,它仍然能访问到外层变量。典型例子如下:

function createCounter() {
  let count = 0;
  return function() {
    count++;
    return count;
  };
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2

之所以 counter 在 createCounter 执行结束后还能操作 count,就是因为返回的匿名函数保存了 count 所在的作用域引用。闭包常用于数据封装、回调函数和函数工厂,但如果滥用闭包,也会让外层变量无法被垃圾回收,造成内存占用上升。

变量提升与暂时性死区

变量提升是 JavaScript 执行模型中的一个重要概念。在代码执行前,引擎会先扫描作用域内的声明,把变量声明提升到作用域顶部。对于 var 来说,提升后变量会被初始化为 undefined,所以在声明之前访问 var 变量不会报错,只会得到 undefined。这一点容易掩盖拼写错误和逻辑顺序问题。

let 和 const 同样存在提升,但它们提升后的行为不同。在声明语句执行之前,变量会进入暂时性死区,简称 TDZ。此时如果访问该变量,会直接抛出 ReferenceError,而不是返回 undefined。例如:

console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 10;

暂时性死区的存在让代码行为更符合直觉,也更容易在开发阶段暴露问题。需要注意的是,TDZ 的边界是块级作用域,即使在块内某一行之前用 typeof 检测变量,如果该变量在块内由 let 声明,也会触发报错,而不是像未声明变量那样返回 'undefined'。

实际编码中的声明选择建议

在现代 JavaScript 项目中,通常建议优先使用 const 声明不会重新赋值的变量。这样既能表达变量不可变的意图,也能减少意外覆盖。只有当变量确实需要改变时才使用 let,例如循环计数器、累加结果、状态切换等。对于 var,除了维护旧代码需要兼容的场景外,新代码应尽量避免使用,因为它缺少块级作用域且容易污染全局。

一个常见的实际问题是循环中创建异步回调。使用 var 时,由于变量是函数作用域且共享同一个引用,循环结束后回调拿到的往往是最后一个值;而 let 会在每次迭代时生成新的绑定,从而保留每一次迭代的正确值。这个差异充分体现了块级作用域在异步编程中的价值。

此外,在函数内部声明变量时,可以借助块级作用域隔离临时逻辑。例如用一对独立花括号把不相关的变量包起来,让它们尽早结束生命周期:

function process(data) {
  let result = [];
  {
    let temp = normalize(data);
    result.push(temp);
  }
  // temp 在这里不可访问
  return result;
}

总之,理解 JavaScript 的声明方式与作用域规则,是写出可维护、可预测代码的前提。把 var 的函数作用域、let 和 const 的块级作用域、变量提升、暂时性死区以及闭包串起来看,才能解释清楚很多表面奇怪的行为。

广告位 · 文章底部(后台-模块管理替换此区域)