順番に読み進めながら学べます

スコープとホイスティング — 変数が読める範囲と巻き上げ

JavaScript の変数をどこから読めるかを決めるスコープと、宣言が実行より先に登録されるホイスティングを扱います。同名変数のシャドーイング、let の TDZ、var を使わない理由まで確かめます。

波括弧の中で計算した値を、その外で使おうとすると実行が止まります。下の行で宣言した設定値を上の行で読んだときも止まり、名前をどこから、いつから使えるのかが分かりにくくなります。

この記事では、宣言した名前をどこから読めるかを決めるスコープと、宣言が実行より先に登録されるホイスティングを扱います。

波括弧の中だけで使える変数 — ブロックスコープ

送料無料まであと何円かをカートの画面に出したいとします。不足額は送料無料の判定に使った if の中で計算できますが、その値を波括弧の外で表示しようとすると ReferenceError で止まります。

スコープ(名前をどこから読めるかを決める範囲)は波括弧で区切られます。波括弧 1 組の範囲をブロックスコープと呼び、constlet で宣言した変数を読めるのは、宣言を囲む一番内側のブロックスコープの中だけです。

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) — 一番外側から関数を呼び出す
checkShipping の波括弧
  • const freeLine = 5000 — この波括弧の中だけで読める
  • 外側の cartTotal はそのまま読める
  • console.log(shortage) — if を出た位置からは読めず ReferenceError
if の波括弧
  • const shortage = 200 — この波括弧の中だけで読める
  • freeLinecartTotal はどちらも読める
内側からは外側の値を読めますが、if の中で作った値は波括弧を出た時点で読めません。読める向きは内側から外側への一方向です

どの波括弧にも囲まれていない一番外側はグローバルスコープで、そこの変数はどこからでも読めます。関数の本体も波括弧なので、freeLine を関数の外から読むことはできません。関数の波括弧で区切られる範囲は特に関数スコープと呼びます。

会員ページに、次のランクまでの残りポイントを表示します。points と nextRankLine は宣言済みです。

① 残りポイントを入れる remaining を、if の外からも読める位置に let で宣言してください。

② ポイントが届いていないときだけ、if の波括弧の中で remaining に残りを代入してください。

③ if の外で「次のランクまであと ◯ pt」の形で表示してください。

(正しく実行できれば解説が表示されます)

JavaScript / TypeScript エディタ

コードを実行してください

内側の宣言が優先される — シャドーイング

軽減税率で計算する関数を足すときに、関数の中で外側と同じ 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(外側は変わっていない)
同じ名前をどこから読むかで変わる
const taxRate= 0.1(外側)withTax の中から読むwithReducedTaxの中から読む関数の外から読む内側に宣言がなく 0.1内側の 0.08が見つかる外側の 0.1がそのまま1100 が返る1080 が返る0.1 が表示
同じ taxRate という名前でも、読む位置によって見つかる宣言が変わります。内側の宣言は外側の値を書き換えません

名前は読む行を囲む内側の波括弧から探され、withReducedTax の中では 0.08 が見つかった時点で探すのをやめるため、外側の 0.1 は読まれません。読み手には同じ名前が関数ごとに違う値を指して見えるので、自分で書くときは reducedTaxRate のように名前を分けます。

const を忘れると外側に代入されます

関数の中で const を付けずに taxRate = 0.08; と書くと、新しい変数は作られず、外側の taxRate への代入になります。外側が const なら TypeError で止まりますが、let だと値が書き換わり、あとで呼んだ withTax(1000) まで 1080 を返します。

商品ページの価格の見出しを、セール中だけ差し替えます。label と isSale は宣言済みです。

① isSale が true のとき、if の波括弧の中で label を「セール価格」として宣言し、表示してください。

② if の外で label を表示してください。

③ 中の名前を saleLabel に変えた if をもう 1 つ書き、「通常価格 → セール価格」の形で表示してください。

JavaScript / TypeScript エディタ

コードを実行してください

宣言の行まで読めない — ホイスティングと TDZ

付与率や送料のような設定値を、処理の下の行にまとめて書きたいとします。関数の記事で見たとおり function 宣言は下に書いても呼べますが、letconst の設定値を宣言の行より前に読むと ReferenceError で実行が止まります。

JavaScript は実行を始める前に、スコープにある宣言の名前を先に登録します。これをホイスティング(宣言が実行より先に登録される仕組み)と呼びます。function 宣言は名前と本体がまとめて登録されるため、宣言より前の行から呼べます。letconst は名前だけが登録され、値が入るのは宣言の行を実行したときです。

// 関数の本体は、呼び出したときに 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 の値が読めるようになる時点
スコープの実行が始まるpointRate の名前だけが登録されるlet の行に届くまでの間calcPoint を呼んでもReferenceErrorlet pointRate= 0.05 を実行以降の呼び出しは160 を返す
let の行を実行するまでは、上に書いた関数の中から読んでも値がありません。読めるかどうかは、書いた位置ではなく実行の順番で決まります

スコープに入ってから let の宣言の行を実行するまでの間を TDZ(Temporal Dead Zone・宣言の行が実行されるまで名前を読めない期間)と呼びます。エラー文が is not defined なら名前がスコープに無く、before initialization なら名前はあっても、宣言の行を実行する前に読んでいます。

注文確認の画面に、送料無料までの金額と付与ポイントを表示します。orderTotal は宣言済みです。

① 送料無料の基準 freeLine を let で 5000 として宣言し、その下の行で「送料無料まで ◯ 円」と表示してください。

② 「付与ポイント: ◯ pt」を表示する行を先に書き、その下の行で付与率 pointRate を let で 0.05 として宣言してください。

③ 何行表示され、どのエラーで止まるかを確かめてください。

JavaScript / TypeScript エディタ

コードを実行してください

undefined で先に登録される — var を使わない理由

古い記事やライブラリでは var での宣言を見かけます。動いているコードをまねすると var が混ざりますが、変数の有効範囲と、宣言より前で読んだときの止まり方が constlet とは違います。

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);
var は波括弧では区切られない
if の中で varstatus に代入if の波括弧を出る関数の波括弧の中なので読める出荷できますと表示されるif の中で letnote に代入if の波括弧を出るnote の範囲はif の中だけReferenceErrorで止まる
同じ if の中で代入しても、var の値は波括弧を出た行から読めます。let なら、範囲の外で読んだ行で止まります

var の変数は関数に 1 つだけなので、if の中で同じ名前を var で宣言し直しても、シャドーイングにはならず同じ変数を書き換えます。下の表は、宣言の書き方ごとに、実行前の登録で入るものと、宣言より前の行で読んだときの結果を並べたものです。

書き方実行前の登録で入るもの宣言より前の行で読んだとき
var stock = 12名前と undefinedundefined のまま次の行へ進む
let stock = 12名前だけ(値は無い)ReferenceError で止まる
const stock = 12名前だけ(値は無い)ReferenceError で止まる

var を let に変えると止まる行が出ます

古いコードの varlet に置き換えると、宣言より前で読んでいた行や、if の外で読んでいた行が ReferenceError で止まります。止まった行は、それまで undefined や別の波括弧の値を読んでいた箇所です。宣言を、読む行より上で、読む行も囲む波括弧の中へ移して直します。

var の宣言位置による違いを確かめます。cartTotal は宣言済みです。

① cartTotal から discount を引いた「お支払い: ◯ 円」を表示する行を先に書き、下で discount を var で 300 と宣言してください。

② その下で、もう一度表示してください。

③ 5000 円以上なら、if の中で discount を var で 800 と宣言し直してください。

④ if の外で「割引額: ◯ 円」を表示してください。

JavaScript / TypeScript エディタ

コードを実行してください
QUIZ

理解度チェック

まずは1問ずつ答えてみましょう。

Q1if (true) { const unit = "円"; } の次の行で unit を表示すると何が起きますか?

Q2rate を返す関数 show を上に書き、let rate = 0.1; の下で呼ぶと何が返りますか?

Q3console.log(count);var count = 3; より前の行に書くと何が表示されますか?