let

Baseline 已广泛支持

此特性已非常成熟,可在多种设备和浏览器版本上使用。自 ⁨2016 年 9 月⁩以来,它已在各大浏览器中可用。

let 声明用于声明可重新赋值的块级作用域局部变量,可以选择将其初始化为一个值。

试一试

let x = 1;

if (x === 1) {
  let x = 2;

  console.log(x);
  // Expected output: 2
}

console.log(x);
// Expected output: 1

语法

js
let name1;
let name1 = value1;
let name1 = value1, name2 = value2;
let name1, name2 = value2;
let name1 = value1, name2, /* …, */ nameN = valueN;

参数

nameN

要声明的变量名。每个变量名都必须是合法的 JavaScript 标识符解构绑定模式

valueN 可选

变量的初始值。它可以是任何合法的表达式。默认值是 undefined

描述

使用 let 声明的变量的作用域是以下最接近包含 let 声明的由大括号括起来的语法之一:

或者如果以上都不适用:

  • 对于在模块模式下运行的代码,为当前模块
  • 对于在脚本模式下运行的代码,为全局作用域。

var 相比,let 声明具有以下区别:

  • let 声明的作用域是块以及函数。

  • let 声明只能在声明位置被执行到之后才能访问(参见暂时性死区)。因此,let 声明通常被认为是不可提升的。

  • 当在脚本的顶层声明时,let 声明不会在 globalThis 上创建属性。

  • let 声明不能在同一作用域内被任何其他声明重新声明

  • let 开启的是_声明_,而不是_语句_。这意味着你不能将独立的 let 声明用作块的主体(这是有道理的,因为没有办法访问该变量)。

    js
    if (true) let a = 1; // SyntaxError: Lexical declaration cannot appear in a single-statement context
    

请注意,在非严格模式下,当使用 varfunction 声明时,let 允许作为标识符名称,但你应该避免使用 let 作为标识符名称,以防止出现意外的语法歧义。

许多代码风格指南(包括 MDN 的)建议,只要变量在其作用域内不被重新赋值,就使用 const 而不是 let。这使得意图明确,即变量的类型(或原始值的值)永远不会改变。其他人可能更喜欢对被修改的非原始值使用 let

let 关键字后面的列表称为_绑定列表_,并用逗号分隔,其中逗号_不是_逗号运算符= 符号_不是_赋值运算符。列表中后面变量的初始化器可以引用列表中较早的变量。

暂时性死区 (TDZ)

使用 letconstclass 声明的变量从块的开始到代码执行到达变量声明和初始化的地方,都被认为处于“暂时性死区”(TDZ)中。

在 TDZ 内部,变量尚未用值初始化,任何尝试访问它的行为都将导致 ReferenceError。当执行到达代码中声明变量的位置时,变量将用值初始化。如果变量声明时未指定初始值,它将用 undefined 值初始化。

这与 var 变量不同,var 变量如果在声明之前访问,将返回 undefined 值。下面的代码演示了在声明位置之前访问 letvar 时,代码中结果的不同。

js
{
  // TDZ starts at beginning of scope
  console.log(bar); // "undefined"
  console.log(foo); // ReferenceError: Cannot access 'foo' before initialization
  var bar = 1;
  let foo = 2; // End of TDZ (for foo)
}

使用“暂时性”一词是因为该区域取决于执行顺序(时间),而不是代码编写顺序(位置)。例如,下面的代码有效,因为尽管使用 let 变量的函数出现在变量声明之前,但该函数是在 TDZ 之外_调用_的。

js
{
  // TDZ starts at beginning of scope
  const func = () => console.log(letVar); // OK

  // Within the TDZ letVar access throws `ReferenceError`

  let letVar = 3; // End of TDZ (for letVar)
  func(); // Called outside TDZ!
}

对 TDZ 中的变量使用 typeof 运算符会抛出 ReferenceError

js
{
  typeof i; // ReferenceError: Cannot access 'i' before initialization
  let i = 10;
}

这与对未声明变量和值为 undefined 的变量使用 typeof 不同。

js
console.log(typeof undeclaredVariable); // "undefined"

注意: letconst 声明仅在当前脚本被处理时才会被处理。如果您在同一 HTML 中有两个以脚本模式运行的 <script> 元素,则第一个脚本不受第二个脚本中声明的顶级 letconst 变量的 TDZ 限制,但如果您在第一个脚本中声明了一个 letconst 变量,在第二个脚本中再次声明它将导致重新声明错误

重新声明

let 声明不能与任何其他声明在同一作用域内,包括 letconstclassfunctionvarimport 声明。

js
{
  let foo;
  let foo; // SyntaxError: Identifier 'foo' has already been declared
}

函数体内的 let 声明不能与参数同名。catch 块内的 let 声明不能与 catch 绑定的标识符同名。

js
function foo(a) {
  let a = 1; // SyntaxError: Identifier 'a' has already been declared
}
try {
} catch (e) {
  let e; // SyntaxError: Identifier 'e' has already been declared
}

如果你在 REPL(例如 Firefox Web 控制台(工具 > Web 开发者 > Web 控制台))中进行实验,并在两个不同的输入中运行两个同名的 let 声明,你可能会得到相同的重新声明错误。有关此问题的进一步讨论,请参阅 Firefox bug 1580891。Chrome 控制台允许在不同的 REPL 输入之间重新声明 let

你可能会在 switch 语句中遇到错误,因为只有一个块。

js
let x = 1;

switch (x) {
  case 0:
    let foo;
    break;
  case 1:
    let foo; // SyntaxError: Identifier 'foo' has already been declared
    break;
}

为了避免错误,请将每个 case 包装在一个新的块语句中。

js
let x = 1;

switch (x) {
  case 0: {
    let foo;
    break;
  }
  case 1: {
    let foo;
    break;
  }
}

示例

作用域规则

通过 let 声明的变量在其声明的块以及任何包含的子块中都具有作用域。通过这种方式,let 的工作方式与 var 非常相似。主要区别在于 var 变量的作用域是整个封闭函数。

js
function varTest() {
  var x = 1;
  {
    var x = 2; // same variable!
    console.log(x); // 2
  }
  console.log(x); // 2
}

function letTest() {
  let x = 1;
  {
    let x = 2; // different variable
    console.log(x); // 2
  }
  console.log(x); // 1
}

在程序和函数的顶层,letvar 不同,它不会在全局对象上创建属性。例如:

js
var x = "global";
let y = "global";
console.log(this.x); // "global"
console.log(this.y); // undefined

TDZ 与词法作用域结合

以下代码在所示行会产生 ReferenceError

js
function test() {
  var foo = 33;
  if (foo) {
    let foo = foo + 55; // ReferenceError
  }
}
test();

if 块被评估,因为外部的 var foo 有一个值。然而,由于词法作用域,这个值在块内部不可用:if 块_内部_的标识符 foolet foo。表达式 foo + 55 抛出 ReferenceError,因为 let foo 的初始化尚未完成——它仍然在暂时性死区。

在以下情况中,这种现象可能会令人困惑。指令 let n of n.a 已经位于 for...of 循环块的作用域内。因此,标识符 n.a 被解析为指令自身第一部分(let n)中 n 对象的属性 a。这仍然处于暂时性死区,因为其声明语句尚未被执行和终止。

js
function go(n) {
  // n here is defined!
  console.log(n); // { a: [1, 2, 3] }

  for (let n of n.a) {
    //          ^ ReferenceError
    console.log(n);
  }
}

go({ a: [1, 2, 3] });

其他情况

当在块内部使用时,let 将变量的作用域限制在该块中。请注意与 var 的区别,var 的作用域在其声明的函数内部。

js
var a = 1;
var b = 2;

{
  var a = 11; // the scope is global
  let b = 22; // the scope is inside the block

  console.log(a); // 11
  console.log(b); // 22
}

console.log(a); // 11
console.log(b); // 2

然而,下面的 varlet 声明的组合是一个 SyntaxError,因为 var 没有块级作用域,导致它们在同一个作用域内。这导致了变量的隐式重新声明。

js
let x = 1;

{
  var x = 2; // SyntaxError for re-declaration
}

使用解构进行声明

每个 = 的左侧也可以是一个绑定模式。这允许一次创建多个变量。

js
const result = /(a+)(b+)(c+)/.exec("aaabcc");
let [, a, b, c] = result;
console.log(a, b, c); // "aaa" "b" "cc"

欲了解更多信息,请参阅解构

规范

规范
ECMAScript® 2026 语言规范
# sec-let-and-const-declarations

浏览器兼容性

另见