はじめに
参画先のキャッチアップで JavaScript を学んでいたところ、ホイスティング(巻き上げ)という用語に出会い、詳しく知りたくなったので、まとめてみました。
※誤りありましたら、ご指摘いただけますと幸いです。
対象読者
JavaScript 初学者
ホイスティングとは
ホイスティング(Hoisting) とは、JavaScript において 変数や関数の宣言がスクリプトの実行前に自動的に最上部へ移動される という仕様のことを指します。
例えば、次のようなコードは エラーにならずに動作 します。
console.log(a); // undefined
var a = 10;
console.log(a); // 10
Java を経験した身からするとこの時点でだいぶ違和感があると思います。
本来なら変数の宣言よりも前に参照しようとするとコンパイルエラーが起きるはず、、
このような挙動になるのは、変数aの宣言がスクリプトの先頭に巻き上げられているためです。
ただし、var 以外の let や const だとまた挙動が異なります。
変数のホイスティング
var, let, const によってホイスト後の挙動が異なるので確認してみます。
まずそもそもこれら var、let、const についてですが、簡単に説明すると、
-
var
再宣言(宣言後同じ変数名で再度宣言すること)・再代入が可能。 -
let
再宣言は不可・再代入が可能。 -
const
再宣言・再代入共に不可(定数のイメージ)
というような違いがあります。
ただし、上記からわかるように var は意図しない再宣言や再代入でエラーが発生する可能性も高まります。
そのため、基本は let と constが使われるケースが大半なようです。
それでは、本題に戻ります。
varの場合
始めに var の場合です。
上でも見た通り、エラーは発生しませんでした。
下記のコードは、上の図を踏まえて、ホイストされたイメージになります。
var a; // 先頭に巻き上げられている ※宣言部分のみ
console.log(a); // undefined
a = 10; // ここで初めて代入
console.log(a); // 10
この宣言部分のみホイストされているのが後半重要になります。
(参考)undefinedとnull
undefined は簡単に言うと未定義な状態を指します。今回のように未初期化等のケースがまさしくそうです。
ちなみに JavaScript には null もあります。null は意図的に値なしと示したい時に使います。
let / constの場合
続いて、let / constの場合です。
console.log(b); // ReferenceError
let b = 20;
console.log(b);
console.log(c); // ReferenceError
const c = 30;
console.log(c);
この2種類で宣言された変数は、使えるのは宣言以降になります。
よって、宣言前にアクセスしようとするとエラーが発生します。
なぜ??
var はエラーが起きないのに、let と const はエラーが起きました。
ちなみに var のホイスト説明の際にも触れていますが、var でも let でも const でもホイストはされています。
ただしホイストされるのは定義だけです。代入部分はホイストされません。
この前提が重要です。
これらを踏まえて理由は **TDZ(Temporal Dead Zone)**にあります。(物騒な名前、、)
MDNは下記のように書いてあります。
let または const 変数は、ブロックの始まりからコードが実行されて変数が宣言され初期化される行に到達するまでは、「一時的なデッドゾーン」(TDZ) 内にあると言います。
TDZ の中にいる間は、変数は値で初期化されておらず、何かアクセスしようとすると ReferenceError が発生することになります。変数は、宣言されたコードの行まで実行されると、値で初期化されます。変数宣言で初期値を指定しなかった場合は、 undefined という値で初期化されます。
var で宣言された変数が undefined の値で始まるのとは異なり、これらの変数は定義が評価されるまでは初期化されません。以下のコードは、let と var が宣言された行より前のコードでアクセスされた場合に、異なる結果が得られることを示しています。
要するに、let と const の場合は初期化されるまではTDZにあるとみなされるため、エラーが起きていたようです。
関数のホイスティング
関数もホイストされますが、これにも種類があり、通常の関数と**関数式(関数リテラル)**で挙動が変わります。
ちなみにここでは、通常の関数を 「function()...」 で始まるものとします。
関数式は下記の例を見てもらえればと思うのですが、JavaScript では関数を変数に代入することができます。
これもJavaをやってた身とするとなんか変な感じがするんですが、JavaScript において、関数はデータ型の一種です。
つまり、このような変数代入以外にも、数値や文字列と同じように、関数の引数に関数を渡したり、戻り値として関数を返すことも可能です。
脱線しましたが、このような前提で話を戻します。
通常の関数
まずは通常の関数の場合です。
hello(); // "こんにちは!"
function hello() {
console.log("こんにちは!");
}
関数の定義ごとホイストされるため、宣言前に呼び出しても正常に動作します。
関数式(関数リテラル)
続いて関数式の場合です。
greet(); // ReferenceError
const greet = function() {
console.log("こんにちは!");
};
エラーが発生しました。
関数式の場合は、変数のホイストルールに従うため、今回で言うと const で宣言された greet は、TDZにあるとみなされ、エラーが発生します。
まとめ
以前 JavaScript を実装していた際は全く整理ができておらず、関数が呼び出せない?こっちは呼び出せてるのに、、と悩んだことがありましたがこれが原因だったようです。
これくらいの関数であれば見通しもいいのですが、JavaScript はスコープ等も難解な部分があり、コード量が増えれば増えるほど、上記などの理解が重要になってきます。
まだまだ分からないことばかりですが引き続き頑張ります。