JavaScript变量声明:var、let与const深度解析

作者:拾光时间:2026-10-25 14:56:00浏览:475

本文深入解析JavaScript中var、let和const三种变量声明方式的核心差异。重点探讨var的函数作用域与变量提升机制,以及ES6引入的let和const所具备的块级作用域、暂时性死区(TDZ)等特性。通过对比分析,明确const在基础类型与引用类型中的行为差异,并提供现代前端开发中变量声明的最佳实践建议。

前言

在程序运行期间,变量的声明贯穿整个生命周期。任何计算过程都至少涉及一个变量,而计算结果可能关联到一个或多个其他变量。使用变量前必须对其进行声明,这一过程在底层涉及内存空间与地址的分配。既然有分配,就必然伴随回收与再分配。由此可见,变量声明是学习任何编程语言时起步且至关重要的一环。

本文将重点探讨JavaScript语言中关于变量声明的相关机制与细节。

内容

JavaScript的变量声明

尽管大多数编程语言的语法演进中,变量声明作为基础语法通常较为稳定,设计者往往在初始版本中依据设计理念确立其语法,以确保语言长期的稳定性。然而,JavaScript并未拥有这样一个完美的起点。

JavaScript最初被设计为一种仅需10天开发周期的简单网页交互脚本语言,其初期设计缺乏长远规划,显得稚嫩。设计者未曾预料到,JavaScript未来会在互联网生态中占据核心地位,不仅限于浏览器端,更广泛应用于移动端、服务端及桌面应用等领域。

随着JavaScript逐渐受到关注,其语法设计也在不断演进。变量声明方式已从早期原始的var声明,发展为符合ECMAScript标准的let与const声明方式。

接下来,我们将逐一讨论这些声明方式的具体特性。

var的变量声明

用于声明函数作用域或全局作用域的变量,并可同时初始化值。在ECMAScript2015标准之前,它是唯一的变量声明关键字,见证了前端开发的漫长历程。其核心特性如下:

变量声明在函数作用域中

与多数语言不同,var声明的变量并非处于块级作用域,而是位于最近的函数上下文(即局部函数作用域)中。若变量未在函数内声明,则其作用域归属于全局对象global。

/* === 全局作用域 start === */
/*
  变量globalVar声明在任何函数之外,为全局作用域中的变量
  任何的函数和方法都能访问到这个变量
*/
var globalVar = 'global var';

/* === f1函数作用域 start === */
function f1() {
  /*
    变量localVar1声明在函数f1中,为f1的局部函数作用域中的变量
    只有代码词法分析中的f1函数的内部函数能够访问到这个变量
    在这个作用域中可以访问到全局作用域中的变量globalVar
  */
  var localVar1 = 'local var1';
  /* === f2函数作用域 start === */
  return function f2() {
    /*
      变量localVar2声明在函数f2中,为f2的局部函数作用域中的变量,
      只有代码词法分析中的f2函数的内部函数能够访问到这个变量,
      在这个作用域中可以访问到全局作用域中变量globalVar以及外层f1函数作用域中的localVar1
    */
    var localVar2 = 'local var2';
    return localVar2;
  /* === f2函数作用域 end === */
  }
/* === f1函数作用域 end === */
}

f1();
/* === 全局作用域 end === */

在上述代码中,globalVar作为全局对象global中的全局变量,既可通过变量标识符globalVar直接访问,也可通过global.globalVar或window.globalVar的对象属性方式访问。

此外,上述作用域结构形成了一条global -> f1 -> f2的函数作用域链。函数执行时的变量查找与访问均依据此作用域链规则进行。

变量重复声明

var允许在同一作用域内多次声明同一变量。此时,仅首次声明生效,后续声明因变量已存在而被忽略。但若声明时包含初始赋值,该赋值操作仍会正常执行。

/*
  变量的首次声明在当前执行上下文对象中新增一个value变量
*/
var value = 1;

/*
  除了变量的首次声明外,其他相同的声明,都不会被执行,
  但是,声明中的初始赋值依旧会被当作正常的赋值操作执行
*/
var value = 2;

console.log(value); // 2

变量声明提升

var具备“提升”(hoisting)特性。JavaScript在执行函数代码前,会扫描当前执行上下文中的所有var声明,并将它们按顺序提升至当前上下文(即函数顶部)。这种机制使得在声明前即可访问变量。

尽管声明被提升,但初始赋值操作并未提升,导致声明与赋值在逻辑上割裂。即在变量声明前访问变量时,其值为undefined。只有执行到声明与赋值语句后,变量才会被赋予初始值。

/*
  由于变量声明提升了,可以在变量声明前访问使用变量,
  但是,访问到的变量值是undefined
*/
console.log(value); // undefined

/* 变量赋值并没有被提升,只有执行完这一句语句,变量才被赋值为1 */
var value = 1;

console.log(value); // 1

怪异危险的var

var的这些特性虽在语法上合法,但违背常规逻辑,易导致调试困难。例如,变量管理必须以函数为基本单位;重复声明使得变量安全访问与值确认变得难以控制。

尽管可通过编程习惯进行约束,但这仅是软约束。随着JavaScript构建复杂应用的需求增加,这些约束日益显得力不从心。

let和const的变量声明

随着ECMAScript2015的发布,JavaScript变量声明迎来了变革。let与const两个新关键字的引入,直接成为最佳的变量声明方式。

这一变革源于var的局限性,而let与const带来了以下共同新特性:

块级作用域

let与const带来的首个显著特性是块级作用域。过去受限于var的函数作用域,开发者需在函数内谨慎命名变量,尤其在条件判断与循环中,常需借助立即执行函数来保存变量值。

function f() {
  /*
    由于var基于函数作用域,回调函数内的i变量都为同一个副本,
    下面程序的输出结果为5、5、5、5、5
  */
  for (var i = 0; i < 5; i++) {
    setTimeout(() => { console.log(i); });
  }
    
  /*
    通过立即执行函数来构建基本数据类型的值传递,从而创建新的变量副本,
    下面程序的输出结果为0、1、2、3、4
  */
  for (var i = 0; i < 5; i++) {
    (function(i) {
      setTimeout(() => { console.log(i); }, 0);
    })(i);
  }
}

f();

如今,通过花括号{}配合let与const,可声明限制于块级作用域的变量。if块、while块、function块乃至独立的{}均可作为良好的块级作用域声明区域。此外,相较于var,在全局作用域中使用let与const声明的变量不会添加到全局上下文对象global中,从而提升了变量声明的简洁性与安全性。

/*
  由于使用的基于跨级作用域的let,每次循环体都会产生一个i变量的副本,
  下面程序的输出结果为0、1、2、3、4
*/
for (let i = 0; i < 5; i++) {
  setTimeout(() => { console.log(i); }, 0);
}

if (true) {
  let a = 1;
}
/*
  抛出ReferenceError,
  因为用let定义的a变量限制于块级作用域,只在前面方括号中可以访问
*/
console.log(a); // ReferenceError

/*
  let和const声明在全局作用域中声明的变量,不会被隐晦添加到global对象中,
  保证了全局环境的安全,
*/
let b = 1;
console.log(window.b); // undefined

不可重复声明

与var允许重复声明不同,let与const在同一作用域内禁止对同一标识符重复声明。这一约束避免了同作用域下的变量冲突,消除了潜在的运行问题,使复杂应用中的变量声明更加安全。

let a = 1;
{
  /* 不在同一块级作用域内,而在子级块级作用域内 */
  let a = 1;
}
/*
  和最上面的声明在同一块级作用域内,属于重复声明,
  这条语句执行会抛出SyntaxError
*/
let a = 1; // SyntaxError

暂时性死区

相较于var导致的“先使用后声明”现象,let与const虽具备变量声明提前的特性,但在变量完成声明前访问或修改会抛出ReferenceError错误。

从块级作用域顶部至变量声明语句执行完成前的区域,称为暂时性死区(TDZ,temporal dead zone)。TDZ强制变量遵循“声明后才能使用”的原则,提升了代码的安全性与可读性。

{
  /* === 变量a和b的TDZ start === */
  /*
    抛出ReferenceError,
    TDZ范围内不能读取和修改变量b
  */
  console.log(b); // ReferenceError
  let a = 1;
  /* === 变量a的TDZ end === */
  let b = 2;
  /* === 变量b的TDZ end === */
  console.log(a); // 1
}

需注意,TDZ基于执行顺序(时间)而非代码编写顺序(位置)。只要变量访问与修改的执行发生在声明之后,即为合法。

{
  /* === 变量value的TDZ start ===*/
  function f() {
    console.log(value);
  }

  /* 在DZT内访问和修改变量value,会抛出错误 */

  let value = 1;
  /* === 变量value的TDZ end ===*/
  /*
    函数f中对value的访问发生在TDZ外面,
    所以访问合法
  */
  f(); // 1
}

使用好let和const

let与const的新特性使其成为变量声明的最佳实践。除共同特性外,两者存在显著差异。

简单来说,let和const的区别在于,let用于声明基于块级作用域的变量,而const用于声明基于块级作用域的常量。

  • 使用let声明的变量,在其整个生命周期内可随时进行赋值修改。
  • 使用const声明的变量为引用常量,必须在声明时同时初始化赋值。在生命周期内,无法通过赋值操作修改变量值。

const的不可再赋值特性在不同变量类型下表现各异:

  • 若const声明并初始化的为基础数据类型变量(String、Number、Boolean、Symbol),则该变量后续不可修改任何值,因为基础类型的值修改必须通过赋值实现。
  • 若const声明并初始化为引用数据类型变量(Array、Object、Map、Set),则可随意修改该变量的字段属性。因为引用类型的内容修改(新增、修改、删除属性)不涉及变量本身的赋值操作。
/* let声明的变量可以任意赋值和修改 */
let value1 = 1;
value1 = 2;

/*
  抛出SyntaxError,
  const声明的变量必须赋值初始值
*/
const value2; // SyntaxError

/*
  抛出TypeError,
  const声明的变量在整个变量生命周期内不能再赋值
*/
const value3 = 3;
value3 = 4 // TypeError

/* const声明的引用数据类型变量,依旧可以修改其字段值 */
const value4 = {
  key1: 1,
  key2: 2,
};
value4.key = 3;

实践中,建议优先使用const进行声明,仅在确定变量需重新赋值时才改用let。这有助于清晰识别可能重新赋值的变量,减少潜在bug。

总结

ECMAScript2015引入的let与const带来了块级作用域与暂时性死区等新特性,解决了过去var声明中的怪异问题。从语法与运行层面保障了“声明后才可使用”的安全特性,提升了大型应用中变量声明的安全性。

目前,let与const已成为日常开发中的最佳声明方式。深入理解并熟练运用它们至关重要。

参考资料

  • 《JavaScript高级程序设计》
  • 《你不知道的JavaScript》
  • MDN

到此这篇关于JavaScript变量声明的var、let、const详解的文章就介绍到这了,更多相关JS变量声明var、let、const内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!

作者最新文章