本日はJavaScriptの巻き上げについて学習します。
JSのスコープについては下記入門で解説しております。
巻き上げとは?
JavaScriptでの巻き上げ(hoisting)は、宣言の前に関数または変数を使用できる動作です。
JavaScriptコンパイラは、エラーが発生しないように、変数と関数のすべての宣言を一番上に移動します。
これは巻き上げと呼ばれます。
なぜこの巻き上げを1つの記事として取り上げるのか
それは巻き上げは、多くの開発者にとってJavaScriptの未知のまたは見落とされた概念でもある為です。
中級者ですら巻き上げについてあまり理解されていない方が稀にいらっしゃいます。
しかし、開発者が巻き上げを理解していない場合、プログラムにバグ(エラー)が含まれている可能性があります。
私は初学者のうちに学んでおいた方が賢明であると判断した為、ここで解説する事にします。
しかしそれらには注意しなければいけないルールがあります。
巻き上げの重要な側面の1つは、宣言が値ではなくメモリに格納されます。
変数の可変巻き上げ
JavaScriptでは、宣言されていない変数を実行した場合はundefined(未定義)
の値が割り当てられ、型もundefinedとなります。
ご存知かも知れませんが、undefined
はプリミティブ型の1つであり、変数が宣言されているが、まだ値が割り当てられていないことを指します。
つまり割り当てられていない変数は、初期化されデフォルト値はundefined
となります。
let test1; console.log(test1) // undefined console.log(typeof string); // undefined
宣言されていない変数にアクセスしようとすると、ReferenceError
がスローされます。
console.log(variable);//ReferenceError
現代ではvar
キーワードを使用される事は滅多にありませんが、繋がってきますので、その為にまずは下記の確認をお願いします。
x
の変数を宣言しその値を1
として初期化します。
宣言する前にx
変数に参照します。
console.log(x); // undefined var x = 1;
コードの最初の行ではエラーは発生しません。
その理由は、JavaScriptエンジンが変数宣言をスクリプトの先頭に移動するためとなります。
これは可変巻き上げ(variable hoisting)です。
JavaScriptの実行コンテキストの動作方法が原因で発生します。
プログラムを実行すると、JavaScriptエンジンがコードを読み、コードを実行する前でも変数( x )をメモリに配置し値を初期化します。
変数は初期化されてるためundefined
が返されます。
もう少し分かりやすくいきましょう。
console.log(x); // undefined var x = 1;
JavaScriptは、上記のコードを実行した場合、下記のように変換致します。
var x;
console.log(x);
x = 1;
つまり、変数x
の宣言がコードの先頭に移動され、console.logの時点でx
の値が未定義であるため、undefinedが出力されることになります。
これは、var
で宣言された変数には、他の何かが割り当てられるまでundefined
値が与えられます。
まとめましょう。
JavaScriptの可変巻き上げ(hoisting)は、JavaScriptで変数や関数を宣言する際に、その宣言がスコープの先頭に移動する現象です。
つまり、可変巻き上げによって、変数の宣言がコードの先頭に移動して、変数が宣言される前に使用されてもエラーにならず、undefined
という値が返されます。
宣言がどこにあっても、そのスコープの先頭で処理されたものとして扱われます。
では、letキーワードでの現象を見ていきましょう。
//ReferenceError: Cannot access 'x' before initialization console.log(x); let x = 1;
letキーワードではReferenceErrorを返します。
ReferenceErrorはvar
, let
, const
キーワードで宣言していない変数を参照すると返されるエラーです。
あら?
letでもundefined
ではないの?と思われるかもしれません。
重要なのは、letキーワードではデフォルト値で初期化されていないということです。
そのため、JavaScriptはletキーワードを使用して変数宣言を裏側では引き上げますが、変数を初期化しません。
x
変数がメモリにある事になりますが、初期化されません。
このエラーは、初期化のみが定義されていないことを通知していることに注意する必要があります。
letでは宣言される前に変数を使用すると、結果は全てReferenceErrorとなります。
これはconstの場合も同様です。
console.log(a)// ReferenceError const a = 1;
つまり、letとconstは undefined
を出力する代わりに参照エラーを介して動作します。
変数の作成から初期化までの期間で、変数にアクセスできません。
これは、私たちプログラマーでは 一時的なデッドゾーン
と呼んでいます。
初期化ではなく、宣言のみを巻き上げることを忘れないでください。
つまり、コンパイル時にJavaScriptは関数と変数の宣言のみをメモリに保存し、それらの割り当て(値)は保存される事はありません。
可変巻き上げの簡単な例は下記になります。
// varは巻き上げられる a = 5; console.log(a); var a; // 5 // letは巻き上げられない a = 5; console.log(a); let a; // ReferenceError // constは巻き上げられない a = 5; console.log(a); const a; // syntax error
上記のconstのように、JavaScriptは別のエラーをスローすることに注意してください、変数を処理するときのJavaScriptの動作は、巻き上げのために微妙になったりします。
すべての変数と関数の宣言はスコープの一番上に引き上げられます。
ただし、宣言されていない変数は、それらを割り当てるコードが実行されるまで存在すらしません。
宣言されていない変数に値を割り当てた場合、割り当ての実行時にその値がグローバル変数として暗黙的にJavaScriptによって作成されます。
これは、宣言されていない変数は全てグローバル変数という事です。
function hoist() { a = 10; //グローバルとなる let b = 100; console.log(b); // 100 } hoist(); console.log(a); // 10 console.log(b); //ReferenceError
b
はhoist()
関数の範囲外では出力できません。
つまりhoist()
関数のスコープに限定されています。
a
は宣言されておらず、それはグローバルスコープとなり関数外からでも出力可能です。
変数が関数内にあるか、グローバルスコープ内にあるかに関係なく、常に現在のスコープの最上位で変数を宣言することがベストです。
関数の巻き上げ
関数での例をしてしまいましたが、ここからもう少し例を見て学習していきましょう。
変数と同様に、JavaScriptエンジンも関数宣言を持ち上げます。
関数での巻き上げ回避策は名前付き関数を使用します。
まずは名前付き関数での宣言前の関数呼び出しです。
greetings("Taro", "Hanako"); //Hello, my name is Taro //Nice to meet you Hanako function greetings(test1, test2) { console.log(`Hello, my name is ${test1}`); console.log(`Nice to meet you, ${test2}!`); }
名前付き関数(関数宣言)の場合、宣言する前に関数を使用できます。
varキーワードを使用した巻き上げを見てましょう。
favouriteSport() var apple = "りんご"; function favouriteSport() { console.log(`あなたの好きな果物は? ${apple}`) // あなたの好きな果物は? undefined var sport = "ランニング" console.log(`あなたの好きなスポーツは? ${sport}`) // あなたの好きなスポーツは? ランニング }
これは関数内の変数にのみアクセスできます。
変数は引き上げられundefined
値を提供するまで、この場合では実行中になります。
関数定義の後に宣言された関数はスクリプトの先頭に持ち上げられ、そうすることによってエラーが発生することはありません。
ではletを見てみましょう。
let x = 10, y = 10; let result = test1(x, y); console.log(result); // 20 function test1(a, b) { return a + b; }
上記ではtest1
関数を定義する前に関数を呼び出しています。
これで、コンパイル段階で関数宣言がメモリに追加されている事が分かりました。
関数宣言の前にアクセス可能です。
関数式(無名関数)の巻き上げ
では関数式(無名関数)ではどうなのか、下記では無名関数を使用しています。
let greetings = function () { console.log("Hello World!"); };
それでは巻き上げます。
定義される前に、関数式を呼び出すとエラーが発生します。
greetings(); //TypeError: greetings is not a function let greetings = function () { console.log("Hello World!"); };
無名関数を呼び出す事はできません。
つまり、関数式は引き上げられません。
前述した名前付き関数と同じように変更してみます。
let x = 10, y = 10; let result = test1(x,y); console.log(result); // TypeError: test1 is not a function var test1 = function(x, y) { return x + y; }
実行するとエラーとなります。
これはJavaScriptエンジンがtest1
をメモリ内に変数を生成し、その値をundefined
として初期化する為です。
アロー関数の場合でも、JavaScriptエンジンで関数式と同様にエラーをスローすることに注意してください。
無名関数と名前付き関数での巻き上げは変数には適しておりません、しかし名前付き関数では適していると言えます。
クラスでの巻き上げ
クラスは構築する前に定義する必要があることです。
クラス宣言を使用して定義されたクラスは引き上げられます。つまりJavaScriptにはクラスへの参照があります、ただしクラスはデフォルトでは初期化されていないため、初期化された行が実行される前にクラスを使用するコードはReferenceErrorをスローします。
let foo = new Foo(1, 2) // ReferenceError class Foo { constructor(x, y) { this.x = x this.y = y } }
JavaScriptの関数と変数を宣言する際は、優先順位に気をつけてください。
変数の割り当ては関数宣言よりも優先されまた、関数宣言は変数宣言よりも優先されます。
まとめ
JavaScriptは初期化ではなく、変数は宣言のみを巻き上げるということです。
letおよびconstで宣言された変数は、実行の開始時に初期化されないままですが、varで宣言された変数はundefined
の値で初期化されます。
関数宣言(名前付き関数)は完全に一番上に持ち上げられます、したがって関数を宣言する前に関数を呼び出すことが可能です。
変数および関数式(アロー関数含む)とクラスの式は引き上げられません。
巻き上げは、宣言を移動するだけですが、割り当てはそのまま残され、関数宣言は変数宣言の上に上げられますが、変数割り当ての上には上げられません。
そしてJavaScriptで、厳格モードの場合では、変数が宣言されていない場合、変数を使用する事はできません。
巻き上げに頼るのは非常に便利ですが、誤った使用をするとエラーが発生する可能性があるため、巻き上げに頼る前によく理解するようにしてください。
本日は以上となります。
最後まで読んで頂きありがとうございます。
この記事が気に入ったらブックマークし他の方にも共有して下さい。