「入門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 i
とvar 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;
を参照している。
言われれば当たり前のことだが、慣れないと引っかかりそうな話ではある。