let

Baseline Widely available

This feature is well established and works across many devices and browser versions. It’s been available across browsers since September 2016.

let 文はブロックスコープのローカル変数を宣言します。任意で値を代入して初期化できます。

試してみましょう

構文

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

引数

nameN

宣言する変数または複数の変数の名前です。それぞれは JavaScript の正式な識別子である必要があります。

valueN 省略可

宣言される変数ごとに、任意で初期値を JavaScript の正式な式で指定することができます。

分割代入構文を変数の宣言に使用することもできます。

js
let { bar } = foo; // where foo = { bar: 10, baz: 12 };
// これは、値が 10 の 'bar' という名前の変数を作成します。

解説

let を使用することで、それが使用されたブロックの文または式にスコープを限定した変数を宣言することができます。これは var キーワードのように、変数をブロックスコープに関係なく、グローバルや関数全体のローカルに定義するようなことはありません。他にも varlet の違いは、後者が宣言文に到達した後でしかアクセスできないという点があります(一時的なデッドゾーンを参照)。このため、let 宣言は一般に巻き上げされないと見なされています。

const と同様に、let はグローバル (一番上のスコープ) で宣言されたときに window オブジェクトのプロパティを生成しません

なぜ "let" という名前が選ばれたのかについての説明は、リンク先の StackOverflow の解答で解説されています。

let 変数の多くの課題は、使用されるスコープの先頭で宣言することで避けることができます(そうすることで、読み取り可能な範囲に影響を与えることがあります)。

var とは異なり、let宣言であり、文ではないものです。つまり、単独の let 宣言をブロックの本体として使用することはできません(変数にアクセスする方法がないので、明らかです)。

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

スコープのルール

let で定義された変数は、自身が定義されたブロックと、そこに含まれるサブブロックがスコープになります。この点において let のふるまいは var にとてもよく似ています。大きな違いは、var で定義された変数のスコープはそれを含んでいる関数全体になるということです。

js
function varTest() {
  var x = 1;
  {
    var x = 2; // 同じ変数です!
    console.log(x); // 2
  }
  console.log(x); // 2
}

function letTest() {
  let x = 1;
  {
    let x = 2; // 異なる変数
    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

再宣言

同じ関数やブロックのスコープ内で同じ変数を再宣言すると SyntaxError が発生します。

js
if (x) {
  let foo;
  let foo; // SyntaxError が発生します。
}

switch 文には 1 つのブロックしかないため、エラーが発生してしまうかもしれません。

js
let x = 1;

switch (x) {
  case 0:
    let foo;
    break;
  case 1:
    let foo; // 再宣言のため TypeError
    break;
}

ただし、指摘しておくべき重要な点として、case 節の中で入れ子にしたブロックを使えば、新しいブロックスコープの字句環境を作ることができるため、上記のような再宣言エラーが発生しなくなります。

js
let x = 1;

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

Firefox のウェブコンソール(ツール > ウェブ開発者ツール > ウェブコンソール)などの REPL で実験しているときに、同じ名前の let 宣言を 2 つの入力で実行すると、同じ再宣言エラーが得られる場合があります。この課題については、Firefox bug 1580891でさらなるディスカッションをご覧ください。Chrome コンソールでは、異なる REPL 入力間で let を再宣言することができます。

一時的なデッドゾーン (TDZ)

let または const 変数は、ブロックの始まりからコードが実行されて変数が宣言され初期化される行に到達するまでは、「一時的なデッドゾーン」(TDZ) 内にあると言います。

TDZ の中にいる間は、変数は値で初期化されておらず、何かアクセスしようとすると ReferenceError が発生することになります。変数は、宣言されたコードの行まで実行されると、値で初期化されます。変数宣言で初期値を指定しなかった場合は、undefinedという値で初期化されます。

var で宣言された変数が undefined の値で始まるのとは異なり、これらの変数は定義が評価されるまでは初期化されません。以下のコードは、letvar が宣言された行より前のコードでアクセスされた場合に、異なる結果が得られることを示しています。

js
{
  // TDZ starts at beginning of scope
  console.log(bar); // undefined
  console.log(foo); // ReferenceError
  var bar = 1;
  let foo = 2; // End of TDZ (for foo)
}

コードが書かれた順番(位置)ではなく、実行の順番(時間)に依存するゾーンであるため、「時間的」という言葉が使用されています。例えば、下記のコードは、let 変数を使用する関数は変数宣言の前に現れていますが、その関数が TDZ の外で呼び出されているため、動作します。

js
{
  // TDZ はスコープの先頭から始まる
  const func = () => console.log(letVar); // OK

  // TDZ の中では、letVar にアクセスすると `ReferenceError` が発生する

  let letVar = 3; // TDZ の終わり (letVar)
  func(); // TDZ の外での呼び出し
}

一時的なデッドゾーンと typeof

typeof 演算子を使用して一時的なデッドゾーン内の変数の型を確認するしようとすると、ReferenceError が発生します。

js
// 'ReferenceError' が発生します
console.log(typeof i);
let i = 10;

これは、宣言されていない変数に対して typeof を使用した場合、undefined の値と見なされるのとは異なります。

js
// 'undefined' と表示
console.log(typeof undeclaredVariable);

一時的なデッドゾーンと字句スコープと組み合わせた例

このブロックの foo は一時的なデッドゾーンの中にあります。

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

外側の var foo に値があるため、if ブロックは実行されます。 しかし、字句スコープにより、この値はブロックの内部では利用できません。ifブロックの内側にある識別子 foolet foo になります。 式 foo + 55ReferenceError が発生します。これは let foo の初期化がまだ完了していない、つまり一時的なデッドゾーンにあるためです。

この現象は、以下のような状況で混乱することがあります。 let n of n.a という命令は、すでに for ループのブロックのプライベートスコープ内にあります。 そのため、識別子 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 との違いに注意してください。

js
var a = 1;
var b = 2;

{
  var a = 11; // スコープはグローバル
  let b = 22; // スコープは if ブロック内

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

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

しかし、下記の varlet 宣言の組み合わせは、var がブロックの先頭に配置されているため、SyntaxError になります。これによって、変数が暗黙的に再宣言されるからです。

js
let x = 1;

{
  var x = 2; // 再宣言のため SyntaxError
}

仕様書

Specification
ECMAScript Language Specification
# sec-let-and-const-declarations

ブラウザーの互換性

BCD tables only load in the browser

関連情報