Core JavaScript 2.実行コンテキスト - ホイスティングと関数式

Chap2. 実行コンテキスト

目次

VariableEnvironment

実行コンテキストが生成する時、まずVariableEnvironmentに情報を入れて、それをコピーしてLexicalEnvironmentと作る。 その後は主にLexicalEnvironmentを活用することになる。 各Environmentは、

で構成される。

LexicalEnvironment

lexicalの意味

「語彙的(な)」・「語彙の」

environmentRecordとホイスティング

environmentRecordには以下の識別子情報が保存される。

ある関数の実行コンテキストの場合、

の3つの情報をコンテキスト内部で上から順番に収集する。

識別子情報収集はコード実行する前に行われるため 「JavaScriptは識別子を最上段に巻き上げる」といっても問題ない。

ホイスティングの規則

例1

function a (x) {  // 収集対象1(パラメータ)
  console.log(x); // (1)
  var x;          // 収集対象2(変数宣言)
  console.log(x); // (2)
  var x = 2;      // 収集対象3(変数宣言・割り当て)
  console.log(x); // (3)
}
a(1);

ホイスティングというのが行われないとすると、(1)1(2)undefined(3)2が出力されると思うことができる。

例2

function a () {
  var x = 1;      // 収集対象1(変数宣言・割り当て)
  console.log(x); // (1)
  var x;          // 収集対象2(変数宣言)
  console.log(x); // (2)
  var x = 2;      // 収集対象3(変数宣言・割り当て)
  console.log(x); // (3)
}
a();

例2で収集対象1がパラメータから変数宣言に変わったが、1例1と例2は同じLexicalEnvironmentを構成する。ホイスティングが行われるからだ。

ホイスティング処理は変数宣言を巻き上げて、割り当てはそのまま置いておく。

例2は以下のようにホイスティングされる。

function a() {
  var x;          //収集対象1の変数宣言
  var x;          //収集対象2の変数宣言
  var x;          //収集対象3の変数宣言

  x = 1;          //収集対象1の割り当て
  console.log(x); // (1)
  console.log(x); // (2)
  x = 2;          //収集対象2の割り当て
  console.log(x); // (3)
}

最初(1)1(2)undefined(3)2が出力されると予測したが、実際に(1)1(2)1(3)2が出力される。

例3

function a() {
  console.log(b);  // (1)
  var b = 'bbb';   // 収集対象1(変数宣言・割り当て)
  console.log(b);  // (2)
  function b() { } // 収集対象2(関数宣言)
  console.log(b);  // (3)
}
a();

(1)undefined(2)bbb(3)で関数が出力されそうなコードになっている。

ホイスティング完了後のコードは以下のように変わる。

function a() {
  var b;           // 収集対象1(変数宣言)
  var b = function () { } // 収集対象2(関数宣言)

  console.log(b);  // (1)
  var b = 'bbb';   // 収集対象1(変数割り当て)
  console.log(b);  // (2)
  console.log(b);  // (3)
}
a();

関数宣言は、関数名で宣言した変数に関数を割り当てたような形になる。 実行結果、(1)で関数、(2)bbb(3)bbbが出力される。

関数宣言と関数式

関数を定義する方法には2つ種類がある。

関数宣言と関数式の違い

例4

console.log(sum(1, 2));
console.log(multiply(3, 4));

// 関数宣言
function sum(a, b) {
  return a + b;
}

// 関数式
var multiply = function (a, b) {
  return a * b;
}

ホイスティング結果

var sum = function sum(a, b) { //関数宣言文全体をホイスティングする
  return a + b;
}
var multiply; //変数宣言だけ巻き上げる

console.log(sum(1, 2)); // (1)
console.log(multiply(3, 4)); // (2)

multiply = function (a, b) { //割り当ては巻き上がらない
  return a * b;
}

(1)では問題なく3が出力されるが、 (2)multiply is not function.というエラーメッセージが出力される。

このように関数宣言はホイスティングが行われるため、どこで定義してもどこで定義されて実行できているのか把握しづらくなる。

ホイスティングという概念を理解していルトしても、コードを作成するのはJavaScriptのエンジンでなくプログラマーなので「宣言した後、呼び出すことができる」という概念の方が自然だ。

関数宣言の危険性

ただ違和感の話ではなく、実務で関数宣言が障害の原因になる危険性もある。

console.log (sum(3, 4)); // (1) '3 + 4 = 7'
...
function sum(x, y) {
  return x + y;
}
...
var a = sum(1, 2); // (2) '1 + 2 = 3'
...
function sum(x, y) {
  return x + ' + ' + y + ' = ' + (x + y);
}

var c = sum(1, 2); // (3)  '1 + 2 = 3'

グローバルコンテキストが構成される時同じ変数名でそれぞれ異なる値を割り当てる場合、後から割り当てた値が先に割り当てた値を上書きする(override)。

(1)(2)73を返すことを期待したが、 sum()が上書きされて意図してなかった結果になってしまう。

なので関数式を使って関数定義すると、ホイスティングで割り当て部分が元の行に残るので上から順番に実行され、意図通り動作する。 また、不具合も早い段階で把握することができ、バグを未然に防ぐことができる。

console.log (sum(3, 4)); // Uncaught Type Error: sum is not a fuction.
...
var sum = function (x, y) {
  return x + y;
}
...
var a = sum(1, 2); // (1) 3
...
var sum = function (x, y) {
  return x + ' + ' + y + ' = ' + (x + y);
}

var c = sum(1, 2); // (3)  '1 + 2 = 3'