[EffectiveJavaScript輪読会2]変数の巻き上げ(ホイスティング)を理解する

GAS

どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。

前回のおさらい

前回は、「クロージャと仲良くしよう」をお届けしました。

[EffectiveJavaScript輪読会2]クロージャと仲良くしよう
どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。前回のおさらい前回は、「ローカル宣言は、必ず宣言...

今回は、「変数の巻き上げ(ホイスティング)を理解する」 をお届けします。

テキスト第2章「変数のスコープ」の項目12に対応しています。

今日のアジェンダ

  • レキシカルスコープと名前束縛
  • varとlet宣言
  • 関数の評価
  • 実務を意識するなら

レキシカルスコープと名前束縛

レキシカルスコープとは、ソースコードを実行するまえに、解釈をした時点でスコープを評価する 仕組みのことでした。

このコードのグローバル領域が読み込まれたときには、内部で グローバル環境レコード が作成され、名前結合が行われています。

console.log(globalThis); //{ x: undefined, myFunction2_12_01: [Function: myFunction2_12_01] }

var x = 100;
console.log(globalThis); //{ x: 100, myFunction2_12_01: [Function: myFunction2_12_01] }

let y = 10;

function myFunction2_12_01() {}

レキシカルスコープは、識別子自身が書かれているスコープ内に、変数宣言が存在しなければ、ひとつ上のスコープを探しにいく仕組み(スコープチェーン)でした。

また、識別子yが、「let y = 10;」のyと対応付けされることが、名前束縛でした。

console.log(globalThis); //{ x: undefined, myFunction2_12_01: [Function: myFunction2_12_01] }

var x = 100;
console.log(globalThis); //{ x: 100, myFunction2_12_01: [Function: myFunction2_12_01] }

let y = 10;

function myFunction2_12_02() {

  let num = y * 2; 
  console.log(num); //20

}

varとlet宣言

このようなコードでも、ソースコードを実行する前に、レキシカルスコープとして、スコープ内に識別子と名前束縛される定義があるのかどうかを評価しています。

ソースコードを保存したときに、エラーなどは発生しません。

ただし、ソースコードを実行すると、varとletでは挙動が異なります。

名前束縛をするさいに、varは代入がなければ、未定義を表す「undefined」を初期値を設定するのに対し、letは代入がなければ、「初期化されていない」というフラグを初期値に設定します。

変数を参照し、初期化されていない というフラグが立っていると、リファレンスエラーの例外 をはきます。

function myFunction2_12_03() {

  console.log(first); //undefined
  first = 'Bob';
  var first;

  console.log(second); //ReferenceError: Cannot access 'y' before initialization
  second = 'Tom';
  let second;

}

関数の評価

レキシカルスコープによるスコープの評価は、関数でも同様です。

スコープ内であれば、関数は呼び出しと定義のどちらを先に記述しても構いません。

function myFunction2_12_04() {

  console.log(getLength('abcde')); //5

  function getLength(arg) {
    return arg.length;
  }

  function getUpperCase(arg) {
    return arg.toUpperCase();
  }

  console.log(getUpperCase('abcde')); //ABCDE

}

関数リテラル(関数式)

関数リテラル(関数式)を、呼び出す前に記述しなければならないのは、関数が代入された 変数 だからです。

その挙動は変数の初期値とおなじです。

function myFunction2_12_05() {

  //内部の初期値はundefined
  console.log(getLength('abcde')); //TypeError: getLength is not a function

  var getLength = (arg) => {
    return arg.length;
  }

  //初期化されていないフラグが初期値に立っている
  console.log(getUpperCase('abcde')); //ReferenceError: Cannot access 'getUpperCase' before initialization

  const getUpperCase = (arg) => {
    return arg.toUpperCase();
  }

}

実務を意識するなら

ここまで、「巻き上げ」という言葉を使わずに、JavaScript特有の変数・関数名のレキシカルスコープと名前束縛で挙動を確認しました。

テキストでも、「varは、もっとも近いスコープの先頭で変数定義される」 ような説明がされていますが、結果としてそのように見えるだけで、「巻き上げ」という特別な仕様があるわけではありません。

function myFunction2_12_03() {

  var first;
  console.log(first); //undefined
  first = 'Bob';
  // var first;

}

ES6から、ブロックスコープが導入されましたので、あえてvar宣言で書くことはないと思います。

テキストで紹介されていた 「変数宣言はソースコードの冒頭にまとめて、手動で巻き上げる」 という書き方も不要だと思います。(たまにネットで見かけるのはこういう理由だったんですね)

今まで習った通り、「基本はconst、再代入する予定があるならlet宣言」 でいいと思います。

ただ、過去の人が書いたコードと対面するばあいもあるでしょう。

そんなときに、var宣言には変数が巻き上げられたように見える挙動がある、というのを知っておくのは大事だと思いました。

テキストには、このようなコードが紹介されていました。

function myFunction2_12_06() {

  function isWinner(player, others) {

    var highest = 0;
    for (let i = 0; i < others.length; i++) {

      var player = others[i];
      if (player.score > highest) {
        highest = player.score;
      }
      console.log(player.score); //300 //100
      console.log(highest); //300 //300

    }

    console.log(player.score, highest); //100 300
    return player.score > highest; 
  }

  var players = [
    {
      name: 'Tom',
      score: 10000
    },
    {
      name: 'Bob',
      score: 300
    },
    {
      name: 'John',
      score: 100
    }
  ];

  console.log(isWinner(players.shift(), players)); //false
}

varがletになるだけで、結果が全然違うものになります。

function myFunction2_12_07() {

  function isWinner(player, others) {

    let highest = 0;
    for (let i = 0; i < others.length; i++) {

      let player = others[i]; //varをletに変更
      if (player.score > highest) {
        highest = player.score;
      }
      console.log(player.score); //300 //100
      console.log(highest); //300 //300

    }

    console.log(player.score, highest); //10000 300
    return player.score > highest; 
  }

  const players = [
    {
      name: 'Tom',
      score: 10000
    },
    {
      name: 'Bob',
      score: 300
    },
    {
      name: 'John',
      score: 100
    }
  ];

  console.log(isWinner(players.shift(), players)); //true
}

まとめ

以上で、「変数の巻き上げ(ホイスティング)を理解する」をお届けしました。

テキストは、ES6以前に対応していますので、大幅に内容を変更してお届けしました。

次回は、「ローカルスコープを作るには即時関数式(IIFE)を使おう」 をお届けします。

参考資料

関数 MDN Hoisting (巻き上げ、ホイスティング) MDN 【JavaScript】 変数・関数の巻き上げってなんだ?

このシリーズの目次

タイトルとURLをコピーしました