20
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

JavaScript: var/letのスコープとletによる巻き上げ

Last updated at Posted at 2020-01-13

「入門JavaScriptプログラミング」
https://www.amazon.co.jp/dp/479815864X
この本でJavaScriptの学習中。

このタイトルをしておきながら
「ES5以前のJavaScriptを理解している人向けに、ES2015以降の機能について紹介する」
というやや凶悪(?)な一面を持つな本だ。

タイトルはその第1章の内容。
もうvarは使わない気がするが、2点知らなかった内容があったので記録に残す。

基本:letとvarのscope

  • varは関数スコープ

if (true) {
  var foo = `bar`;
}

console.log(foo); // "bar"
  • letはブロックスコープ
if (true) {
  let foo = `bar`;
}

console.log(foo); // fooはブロックの外側に存在しないためエラー

この辺はただの前提。以下が本題。

varの関数スコープによる厄介な影響


<ul>
  <li>one</li>
  <li>two</li>
  <li>three</li>
  <li>four</li>
  <li>five</li>
</ul>

...

<script>
  var items = document.querySelectorAll;
  for (var i = 0; i < 5; i++) {
    var li = item[i];
    li.addEventListener('click', () => {
      alert(li.textContent + ':' + i);
    });
  }
</script>

上のコードはhtmlの各リストアイテムをクリックすると、対応したメッセージのアラートが表示される、ということを意図して書かれている。が、実際には期待通りには動作しない。どのリストアイテムをクリックしても、表示されるのは"five: 5"になる。

var ivar liは関数スコープなので、5回のforループの全てで共有されている。
つまりどのリストアイテムをクリックしても、結局は同じメッセージのアラートが表示される。
forの最後のループが終わった際、'i = 4'の時のループが終わった際には、i = 5, li = <li>five</li>になっているため、前述の通り必ず"five: 5"が表示される。

varの代わりにletを使えば意図した動作になる。
letはブロックスコープであるため、forの各ブロックごとにlet i, let liは独立して存在することになるからだ。

正直なところもうvarを自分で書くことはないと思うが、既存コードに存在する場合はこの点注意が必要になりそうだ。

letによる変数の巻き上げ

letによる変数による巻き上げとは、「変数が宣言されたスコープの内側では、変数がスコープ全体を消費する」と言うことだ。
これだけだとさっぱり分からないのでコードで説明する。


let num = 10;
const getNum = () {
  return num;
}
console.log( getNum() );

このコードはコンソールに10を表示する。getNum()内にnumの宣言が無い為、スコープチェーンの仕組みに則って、外側にあるlet num = 10;が用いられる。

let num = 10;
const getNum = () {
  let num = 5 // 変更箇所
  return num;
}
console.log( getNum() );

この場合はコンソールに5が表示される。これはまだ直感的だろう。

ではこれはどうだろう?

// 外側のnumのスコープここから
let num = 10;
const getNum = () {
  // getNum()内のnumのスコープここから
  console.log(num) // 変更箇所
  let num = 5;
  return num;
  // getNum()内のnumのスコープここまで
}
console.log( getNum() );
// 外側のnumのスコープここまで

まずこの場合のconsole.log(num)let num = 10;let num = 5のどちらを参照するか?
正解はlet num = 5だ。この現象が「let による巻き上げ」だ。numがどこで宣言されていようと、numはスコープ全体を消費する。
(巻き上げ自体はvarでも同様のことが言える。varの場合は関数の単位でスコープ全体を消費する)

上のコードの場合もう1つ問題が発生する。宣言の前にnumが参照されるとどうなるか?とということだ。
具体的にはconsole.log(num)let num = 5よりも前に存在している。

この場合console.log(num)はエラーが発生する。letで宣言される関数が、宣言される前にスコープ内で実際にアクセスされた場合は、参照エラーとなる。
varの場合は参照エラーにはならないが値は常に未定義となる。なおこのようなエラーが発生する領域をTDZ(Temporal Dead Zone)と呼ぶ。

ifを絡めた面倒な話

これにifを絡めるとやや面倒な話になる。

let num = 0;
function getNum() {
  if (!num) {
    let num = 1;
  }
  return num;
}
console.log( getNum() );

この場合コンソールに表示されるのは、1...ではなく0になる。
以下のようにスコープを明示すると分かりやすくなる。

let num = 0;
function getNum() {
  if (!num) { // このnumは(スコープチェーンにより"let num = 0;"を参照)
    // if内のブロックのスコープここから
    let num = 1;
    // if内のブロックのスコープここまで
  }
  return num;
}
console.log( getNum() );

let num = 1はif内のブロックでのみ有効であり、ifの条件式におけるnum & return num;let num = 0;を参照している。
言われれば当たり前のことだが、慣れないと引っかかりそうな話ではある。

20
20
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?