本日はJavaScriptの巻き上げについて学習します。
JSのスコープについては下記入門で解説しております。
巻き上げとは?
JavaScriptでの巻き上げは、宣言の前に関数または変数を使用できる動作です。
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として初期化します。
console.log(x); // undefined var x = 1;
宣言する前にx変数に参照します。
コードの最初の行ではエラーは発生しません。
その理由は、JavaScriptエンジンが変数宣言をスクリプトの先頭に移動するためとなります。
これは可変巻き上げです。
JavaScriptの実行コンテキストの動作方法が原因で発生します。
プログラムを実行すると、JavaScriptエンジンがコードを読み、コードを実行する前でも変数(x)をメモリに配置し値を初期化します。
変数は初期化されてるため『undefined(未定義)』が返されます。
つまりvarで宣言された変数には、他の何かが割り当てられるまでundefined値が与えられます。
下記のletキーワードではReferenceErrorを返します。
ReferenceErrorはvar, let, constキーワードで宣言していない変数を参照すると返されるエラーです。
//ReferenceError: Cannot access 'x' before initialization console.log(x); let x = 1;
letでもundefinedではないの?と思われるかもしれません。
重要なのは、letキーワードではデフォルト値で初期化されていないということです。
そのため、JavaScriptはletキーワードを使用して変数宣言を裏側では引き上げますが、変数を初期化しません。
x変数がメモリにある事になりますが、初期化されません。
このエラーは、初期化のみが定義されていないことを通知していることに注意する必要があります。
// ReferenceError: test1 is not defined console.log(test1); let x = 1;
前述した通り宣言していないまたは、存在しない変数にアクセスした場合も別のエラーとなります。
つまりletでは宣言される前に変数を使用すると、結果は全てReferenceErrorとなる。
constの場合も同様です。
console.log(a)//ReferenceError const a = 1;
つまりletとconstはundefinedを出力する代わりに参照エラーを介して動作します。
letとconstはブロックの先頭に上げられますが、初期化されません。
変数の作成から初期化までの期間で、変数にアクセスできません。
これは、私たちプログラマーでは『一時的なデッドゾーン』と呼んでいます。
初期化ではなく、宣言のみを巻き上げることを忘れないでください。
つまり、コンパイル時に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
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で厳密モードの場合では、変数が宣言されていない場合、変数を使用する事はできません。
巻き上げに頼るのは非常に便利ですが、誤った使用をするとエラーが発生する可能性があるため、巻き上げに頼る前によく理解するようにしてください。
巻き上げについてはJavaScriptのMDNでも解説されておりますのでこちらを参照下さい。
Hoisting (巻き上げ、ホイスティング) - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN
Grammar and types - JavaScript | MDN
本日は以上となります。
最後まで読んで頂きありがとうございます。