【JavaScript入門】巻き上げ

 JavaScript の let 変数には、同じスコープ内で同名の変数を宣言するとエラーになるというルールがありますが、もうひとつ注意が必要な点として「巻き上げ」の仕様があります。ブロックの中で同名の変数を新たに宣言すると、たとえ同じ名前であっても、内部のスコープでは別の変数として扱われます。そのために、思わぬタイミングで「未宣言エラー(初期化前にアクセス)」が起きることがあるのです。

1.巻き上げとは

 JavaScript の let 変数は、ブロックや関数スコープ内で宣言されると、宣言箇所より上に書かれたコードからは利用できません。しかし、内部的にはスコープの最上部で変数が「確保」されているような動作になり(これを巻き上げ/hoisting と呼ぶことがあります)、宣言より上のコードで参照するとエラーが発生する場合があります。

2.実例:巻き上げによるエラー

 以下のコードでは、外側のブロックで item という変数を宣言しています。その後、内側のブロックでも item を宣言しているため、内側のブロックの冒頭で console.log(item) を呼び出すと「初期化前に参照している」というエラーが起きます。

【myHoistingSample.html】

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>myHoistingSample.html</title>
</head>
<body>
  <h1>巻き上げのデモ</h1>
  <p>ブラウザの開発者ツール(コンソール)を開いて結果を確認してください。</p>
  <script>
    let isHoliday = true;
    if (isHoliday) {
      let item = 'スマホ';
      console.log('階層1:', item);

      if (isHoliday) {
        // ここで内側の item が優先され、まだ宣言されていない扱いとなる
        console.log('  階層2:', item); 
        let item = 'タブレット';
      }
    }
  </script>
</body>
</html>

実行結果

デバッグコンソールの出力

階層1: スマホ
Uncaught ReferenceError ReferenceError: Cannot access 'item' before initialization

 上記を実行すると、「階層1: スマホ」という出力はコンソールに表示されますが、その後で console.log(' 階層2:', item); の部分がエラーを引き起こします。内側のブロックが始まった時点で新たに宣言される let item = 'タブレット' が「巻き上げ」によって「存在はしているが、まだ初期化前」な状態とみなされ、アクセスできないとみなされるためです。

まとめ

  • let 変数はスコープの開始部分で定義されているように扱われる(巻き上げ)が、実際に初期化されるのは宣言文に到達したときになります。そのため、宣言より前でアクセスすると「初期化されていない」というエラーになる。
  • 外側のスコープと内側のスコープで同名の変数を宣言した場合、内側の変数が優先して評価されます(シャドーイング)。外側で宣言したものが即座に無効になるわけではありませんが、内側の変数が存在する範囲内ではそちらが使われます。
  • それほど頻繁に問題になるわけではありませんが、大規模なコードや複雑な入れ子がある場合、知らないと「なぜ動かないのかわからない」事態に陥ることがあります。

 こうした仕様があると理解しておくことで、宣言前に変数を参照しようとしてハマるバグを回避できます。JavaScript でブロックスコープを扱うときは、巻き上げの振る舞いを念頭に置き、変数は使う直前ではなく「スコープの先頭付近」で宣言するなどの工夫をすると安全です。