JavaScript関数の宣言と評価順序


80

これらの例の最初の例が機能しないのに、他のすべての例は機能するのはなぜですか?

// 1 - does not work
(function() {
setTimeout(someFunction1, 10);
var someFunction1 = function() { alert('here1'); };
})();

// 2
(function() {
setTimeout(someFunction2, 10);
function someFunction2() { alert('here2'); }
})();

// 3
(function() {
setTimeout(function() { someFunction3(); }, 10);
var someFunction3 = function() { alert('here3'); };
})();

// 4
(function() {
setTimeout(function() { someFunction4(); }, 10);
function someFunction4() { alert('here4'); }
})();

回答:


182

これはスコープの問題でも、クロージャの問題でもありません。問題は、宣言式の間の理解にあります

JavaScriptコードは、NetscapeのJavaScriptの最初のバージョンとMicrosoftの最初のコピーでさえ、2つのフェーズで処理されます。

フェーズ1:コンパイル-このフェーズでは、コードは構文ツリー(およびエンジンに応じてバイトコードまたはバイナリ)にコンパイルされます。

フェーズ2:実行-解析されたコードが解釈されます。

関数宣言の構文は次のとおりです。

function name (arguments) {code}

引数はもちろんオプションです(コードもオプションですが、そのポイントは何ですか?)。

ただし、JavaScriptでは、を使用して関数を作成することもできます。関数式の構文は、式のコンテキストで記述されることを除いて、関数宣言に似ています。そして、式は次のとおりです。

  1. =記号の右側(または:オブジェクトリテラル上)にあるもの。
  2. 括弧内はすべて()
  3. 関数へのパラメーター(これは実際にはすでに2でカバーされています)。

宣言とは異なり、はコンパイルフェーズではなく実行フェーズで処理されます。このため、表現の順序が重要になります。

したがって、明確にするために:


// 1
(function() {
setTimeout(someFunction, 10);
var someFunction = function() { alert('here1'); };
})();

フェーズ1:コンパイル。コンパイラーは、変数someFunctionが定義されていることを確認して作成します。デフォルトでは、作成されるすべての変数の値はundefinedです。値は、割り当てる値を返すために何らかのコードを実行するためにインタープリターを必要とする可能性があるため、コンパイラーはこの時点ではまだ値を割り当てることができないことに注意してください。そしてこの段階では、まだコードを実行していません。

フェーズ2:実行。インタプリタは、変数someFunctionをsetTimeoutに渡したいことを認識します。そしてそうです。残念ながら、の現在の値someFunctionは未定義です。


// 2
(function() {
setTimeout(someFunction, 10);
function someFunction() { alert('here2'); }
})();

フェーズ1:コンパイル。コンパイラーは、someFunctionという名前の関数を宣言していることを認識し、それを作成します。

フェーズ2:インタープリターはsomeFunction、setTimeoutに渡す必要があることを確認します。そしてそうです。の現在の値someFunctionは、コンパイルされた関数宣言です。


// 3
(function() {
setTimeout(function() { someFunction(); }, 10);
var someFunction = function() { alert('here3'); };
})();

フェーズ1:コンパイル。コンパイラは、変数を宣言したことを確認しますsomeFunction作成します。以前と同様に、その値は未定義です。

フェーズ2:実行。インタプリタは、後で実行されるように無名関数をsetTimeoutに渡します。この関数では、変数を使用していることがsomeFunctionわかるため、変数のクロージャが作成されます。この時点では、の値someFunctionはまだ定義されていません。次に、に関数を割り当てていることがわかりますsomeFunction。この時点で、の値はsomeFunction未定義ではなくなりました。100分の1秒後に、setTimeoutがトリガーされ、someFunctionが呼び出されます。その値はもはや未定義ではないため、機能します。


ケース4は、実際にはケース2の別のバージョンであり、ケース3のビットがスローされsomeFunctionています。setTimeoutに渡される時点で、宣言されているため、すでに存在しています。


追加の説明:

setTimeout(someFunction, 10)someFunctionのローカルコピーとsetTimeoutに渡されたコピーの間にクロージャを作成しないのはなぜか疑問に思うかもしれません。その答えは、JavaScriptで関数の引数は、常にあるということです、常に彼らは数字や文字列や他のすべてのために参照することによってである場合に値渡し。したがって、setTimeoutは、実際には変数someFunctionが渡されるのではなく(クロージャが作成されることを意味します)、someFunctionが参照するオブジェクト(この場合は関数)のみを取得します。これは、クロージャを解除するためにJavaScriptで最も広く使用されているメカニズムです(ループなど)。


7
それは真剣に素晴らしい答えでした。
マットブリッグス

1
@マット:私はこれをSOの他の場所(数回)で説明しました。私のお気に入りの説明の一部:stackoverflow.com/questions/3572480/...
slebetman


3
@Matt:技術的には、クロージャにはスコープではなくスタックフレーム(アクティベーションレコードとも呼ばれます)が含まれます。クロージャは、スタックフレーム間で共有される変数です。スタックフレームは、オブジェクトがクラス化する対象をスコープすることです。言い換えると、スコープは、プログラマーがコード構造で認識するものです。スタックフレームは、実行時にメモリに作成されるものです。そんなことはありませんが、十分に近いです。実行時の動作について考えるとき、スコープベースの理解だけでは不十分な場合があります。
slebetman 2010年

3
例3の説明のために@slebetmanは、setTimeout内の無名関数がsomeFunction変数のクロージャを作成し、この時点ではsomeFunctionがまだ定義されていないことを述べています。これは理にかなっています。例3がundefinedを返さない唯一の理由は、setTimeout関数(10ミリ秒の遅延によりJavaScriptがsomeFunctionへの次の代入ステートメントを実行して定義済みにする)が原因のようです。
wmock 2013年

2

Javascriptのスコープは関数ベースであり、厳密な字句スコープではありません。つまり、

  • somefunction1は、囲んでいる関数の最初から定義されていますが、その内容は割り当てられるまで定義されていません。

  • 2番目の例では、割り当ては宣言の一部であるため、「上に移動」します。

  • 3番目の例では、変数は匿名の内部クロージャーが定義されているときに存在しますが、10秒後まで使用されず、それまでに値が割り当てられます。

  • 4番目の例には、2番目と3番目の両方の理由があります


1

someFunction1呼び出しsetTimeout()が実行された時点ではまだ割り当てられていないためです。

someFunction3も同様のケースのように見えるかもしれませんが、この場合は関数の折り返しsomeFunction3()を渡すためsetTimeout()、への呼び出しsomeFunction3()は後で評価されます。


しかしsomeFunction2、への呼び出しsetTimeout()が実行されたときもまだ割り当てられていません...?
私たちはすべてモニカです

1
@jnylen:functionキーワードを使用して関数を宣言することは、無名関数を変数に割り当てることとまったく同じではありません。として宣言された関数function foo()は、現在のスコープの先頭に「引き上げられ」ますが、変数の割り当ては、それらが書き込まれた時点で行われます。
チャック

1
特別な機能の場合は+1。しかし、それ機能するからといって、それ実行されるべきであるという意味ではありません。使用する前に必ず宣言してください。
mway 2010年

@mway:私の場合、コードを「クラス」内のセクション(プライベート変数、イベントハンドラー、プライベート関数、パブリック関数)に編成しました。プライベート関数の1つを呼び出すには、イベントハンドラーの1つが必要です。私にとって、このようにコードを整理しておくことは、宣言を字句的に順序付けるよりも優先されます。
私たちはすべてモニカです

1

これは、トラブルを避けるために適切な手順に従う基本的なケースのように聞こえます。変数と関数を使用する前に宣言し、次のような関数を宣言します。

function name (arguments) {code}

varで宣言することは避けてください。これはただずさんで、問題につながります。使用する前にすべてを宣言する習慣を身につけると、問題のほとんどは急いで消えます。変数を宣言するときは、すぐに有効な値で初期化して、未定義のものがないことを確認します。また、関数がグローバル変数を使用する前に、グローバル変数の有効な値をチェックするコードを含める傾向があります。これは、エラーに対する追加の保護手段です。

これらすべてがどのように機能するかについての技術的な詳細は、手榴弾で遊んだときにどのように機能するかという物理学のようなものです。私の簡単なアドバイスは、そもそも手榴弾で遊んではいけないということです。

コードの先頭にあるいくつかの単純な宣言は、これらの種類の問題のほとんどを解決する可能性がありますが、コードのクリーンアップが必要な場合もあります。

追記:
いくつかの実験を実行しましたが、ここで説明する方法ですべての関数を宣言する場合、それらがどのような順序であるかは実際には問題ではないようです。関数Aが関数Bを使用する場合、関数Bは関数Aの前に宣言されます。

したがって、最初にすべての関数を宣言し、次にグローバル変数を宣言してから、他のコードを最後に配置します。これらの経験則に従ってください。間違いはありません。これらのルールを確実に適用するために、Webページの先頭に宣言を配置し、本文に他のコードを配置するのが最善の場合もあります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.