Q1if (true) { const unit = "円"; } の次の行で unit を表示すると何が起きますか?
スコープとホイスティング — 変数が読める範囲と巻き上げ
JavaScript の変数をどこから読めるかを決めるスコープと、宣言が実行より先に登録されるホイスティングを扱います。同名変数のシャドーイング、let の TDZ、var を使わない理由まで確かめます。
波括弧の中で計算した値を、その外で使おうとすると実行が止まります。下の行で宣言した設定値を上の行で読んだときも止まり、名前をどこから、いつから使えるのかが分かりにくくなります。
この記事では、宣言した名前をどこから読めるかを決めるスコープと、宣言が実行より先に登録されるホイスティングを扱います。
波括弧の中だけで使える変数 — ブロックスコープ
送料無料まであと何円かをカートの画面に出したいとします。不足額は送料無料の判定に使った if の中で計算できますが、その値を波括弧の外で表示しようとすると ReferenceError で止まります。
スコープ(名前をどこから読めるかを決める範囲)は波括弧で区切られます。波括弧 1 組の範囲をブロックスコープと呼び、const と let で宣言した変数を読めるのは、宣言を囲む一番内側のブロックスコープの中だけです。
const cartTotal = 4800; // どの波括弧にも囲まれていない一番外側
function checkShipping(total) {
const freeLine = 5000; // checkShipping の波括弧の中だけ
if (total < freeLine) {
const shortage = freeLine - total; // if の波括弧の中だけ
console.log(`あと ${shortage} 円`); // あと 200 円
}
// 外側で宣言した変数は、内側から読める
console.log(cartTotal); // 4800
// console.log(shortage); // ReferenceError: shortage is not defined
}
checkShipping(cartTotal);
const cartTotal = 4800— ファイルのどこからでも読めるcheckShipping(cartTotal)— 一番外側から関数を呼び出す
const freeLine = 5000— この波括弧の中だけで読める- 外側の
cartTotalはそのまま読める console.log(shortage)— if を出た位置からは読めずReferenceError
const shortage = 200— この波括弧の中だけで読めるfreeLineとcartTotalはどちらも読める
if の中で作った値は波括弧を出た時点で読めません。読める向きは内側から外側への一方向です。どの波括弧にも囲まれていない一番外側はグローバルスコープで、そこの変数はどこからでも読めます。関数の本体も波括弧なので、freeLine を関数の外から読むことはできません。関数の波括弧で区切られる範囲は特に関数スコープと呼びます。
内側の宣言が優先される — シャドーイング
軽減税率で計算する関数を足すときに、関数の中で外側と同じ taxRate という名前の変数を宣言してしまうことがあります。そのときどちらの値が使われるかを知らないと、計算結果がずれた原因を追えません。
内側のスコープで外側と同じ名前を宣言すると、その波括弧の中では内側の宣言が使われます。これをシャドーイング(内側の宣言が外側の同名の変数を隠すこと)と呼び、if の波括弧でも関数の波括弧でも起きます。
const taxRate = 0.1; // 外側で決めた税率
function withTax(price) {
return Math.floor(price * (1 + taxRate));
}
function withReducedTax(price) {
const taxRate = 0.08; // 同じ名前を関数の中で宣言
return Math.floor(price * (1 + taxRate));
}
console.log(withTax(1000)); // 1100
console.log(withReducedTax(1000)); // 1080
console.log(taxRate); // 0.1(外側は変わっていない)
taxRate という名前でも、読む位置によって見つかる宣言が変わります。内側の宣言は外側の値を書き換えません。名前は読む行を囲む内側の波括弧から探され、withReducedTax の中では 0.08 が見つかった時点で探すのをやめるため、外側の 0.1 は読まれません。読み手には同じ名前が関数ごとに違う値を指して見えるので、自分で書くときは reducedTaxRate のように名前を分けます。
const を忘れると外側に代入されます
関数の中で const を付けずに taxRate = 0.08; と書くと、新しい変数は作られず、外側の taxRate への代入になります。外側が const なら TypeError で止まりますが、let だと値が書き換わり、あとで呼んだ withTax(1000) まで 1080 を返します。
宣言の行まで読めない — ホイスティングと TDZ
付与率や送料のような設定値を、処理の下の行にまとめて書きたいとします。関数の記事で見たとおり function 宣言は下に書いても呼べますが、let や const の設定値を宣言の行より前に読むと ReferenceError で実行が止まります。
JavaScript は実行を始める前に、スコープにある宣言の名前を先に登録します。これをホイスティング(宣言が実行より先に登録される仕組み)と呼びます。function 宣言は名前と本体がまとめて登録されるため、宣言より前の行から呼べます。let と const は名前だけが登録され、値が入るのは宣言の行を実行したときです。
// 関数の本体は、呼び出したときに pointRate を読む
function calcPoint(amount) {
return Math.floor(amount * pointRate);
}
// ここで呼ぶと、pointRate の宣言の行にまだ届いていない
// console.log(calcPoint(3200));
// → ReferenceError: Cannot access 'pointRate' before initialization
let pointRate = 0.05; // 設定値を下にまとめて書く
console.log(calcPoint(3200)); // 160(宣言の行を通ったあとなので読める)
let の行を実行するまでは、上に書いた関数の中から読んでも値がありません。読めるかどうかは、書いた位置ではなく実行の順番で決まります。スコープに入ってから let の宣言の行を実行するまでの間を TDZ(Temporal Dead Zone・宣言の行が実行されるまで名前を読めない期間)と呼びます。エラー文が is not defined なら名前がスコープに無く、before initialization なら名前はあっても、宣言の行を実行する前に読んでいます。
undefined で先に登録される — var を使わない理由
古い記事やライブラリでは var での宣言を見かけます。動いているコードをまねすると var が混ざりますが、変数の有効範囲と、宣言より前で読んだときの止まり方が const や let とは違います。
var の宣言も先に登録されますが、そのとき同時に undefined が入ります。そのため宣言より前の行で参照してもエラーにならず、undefined のまま次の行へ進みます。有効範囲も波括弧ではなく、囲んでいる関数スコープ全体です。
function checkStock(orderCount) {
// 宣言より前で読んでも止まらず、undefined が入っている
console.log(stock); // undefined
var stock = 12;
console.log(stock); // 12
if (orderCount <= stock) {
var status = "出荷できます"; // var で宣言
let note = "倉庫 A から出荷"; // let で宣言
}
// var の有効範囲は関数の波括弧全体なので、if の外からも読める
console.log(status); // 出荷できます
// console.log(note); // ReferenceError: note is not defined
}
checkStock(3);
if の中で代入しても、var の値は波括弧を出た行から読めます。let なら、範囲の外で読んだ行で止まります。var の変数は関数に 1 つだけなので、if の中で同じ名前を var で宣言し直しても、シャドーイングにはならず同じ変数を書き換えます。下の表は、宣言の書き方ごとに、実行前の登録で入るものと、宣言より前の行で読んだときの結果を並べたものです。
| 書き方 | 実行前の登録で入るもの | 宣言より前の行で読んだとき |
|---|---|---|
| var stock = 12 | 名前と undefined | undefined のまま次の行へ進む |
| let stock = 12 | 名前だけ(値は無い) | ReferenceError で止まる |
| const stock = 12 | 名前だけ(値は無い) | ReferenceError で止まる |
var を let に変えると止まる行が出ます
古いコードの var を let に置き換えると、宣言より前で読んでいた行や、if の外で読んでいた行が ReferenceError で止まります。止まった行は、それまで undefined や別の波括弧の値を読んでいた箇所です。宣言を、読む行より上で、読む行も囲む波括弧の中へ移して直します。
理解度チェック
まずは1問ずつ答えてみましょう。
Q2rate を返す関数 show を上に書き、let rate = 0.1; の下で呼ぶと何が返りますか?
Q3console.log(count); を var count = 3; より前の行に書くと何が表示されますか?