簡単な質問から始めましょう:
<script type="text/javascript">
アラート(i); //
変数 i = 1;
</script>
出力結果は未定義です。この現象は「事前解析」と呼ばれます。JavaScript エンジンは最初に var 変数と関数定義を解析します。事前解析が完了するまでコードは実行されません。ドキュメント ストリームに複数のスクリプト コード セグメント (スクリプト タグで区切られた JS コード、またはインポートされた JS ファイル) が含まれている場合、実行順序は
次のとおりです。最初のコード セグメントを読み取ります。
step2. 構文解析を実行します。エラーがある場合は、構文エラー (括弧の不一致など) が報告され、step5 に進みます。
step3. var 変数と関数定義の「事前解析」を実行します (正しい宣言のみが解析されるため、エラーは報告されません)。
step4. コードセグメントを実行し、エラーがある場合はエラーを報告します (変数が未定義であるなど)。
ステップ5. 別のコードセグメントがある場合は、次のコードセグメントを読み取り、ステップ2を繰り返します。
step6. 上記の分析を終えると、多くの問題を説明できましたが、何かが足りないといつも感じます。たとえば、ステップ 3 の「事前解析」とは正確には何ですか?ステップ 4 では、次の例を見てください:
<script type="text/javascript">
alert(i); // エラー: i が定義されていません。
i = 1;
</script>
最初の文でエラーが発生するのはなぜですか? JavaScript では変数を未定義にする必要はないのでしょうか?
コンパイルの時間は白馬のように過ぎ、本棚の横にある『コンパイルの原則』をまるで別世界のように開いた。見慣れた、しかし見慣れない空白の中に、次のメモがあった。
伝統的なコンパイル言語用。 、コンパイル手順は、字句解析と構文解析、意味チェック、コードの最適化、バイト生成に分かれています。
しかし、インタープリタ型言語の場合は、字句解析と構文解析を通じて構文ツリーが取得された後、解釈と実行を開始できます。
する
ことです。
等しい
名前「あ」
マイナス
名前「b」
セミコロン
上記は単なる例です。詳細については、
「The Definitive Guide to JavaScript」の第 2 章で、ECMA-262 にも記載されている字句構造について説明しています。語彙構造は言語の基礎であり、習得は簡単です。字句解析の実装については、別の研究領域であるため、ここでは説明しません。
自然言語のアナロジーを使用すると、たとえば、英語の段落を単語ごとに中国語に翻訳すると、大量のトークン ストリームが得られます。理解すること。さらに翻訳するには、文法的な分析が必要です。次の図は、条件文の構文ツリーです。
構文ツリーを構築するときに、if(a { i = 2; } など) 構文ツリーを構築できないことが判明した場合、構文エラーが報告され、コード ブロック全体の解析が終了します。これは、次のステップ 2 です。この記事の冒頭で説明したように、
構文解析を通じて、構文ツリーの後に翻訳された文があいまいになる可能性があり、さらにセマンティック チェックが必要になります。従来の厳密に型付けされた言語の場合、セマンティック チェックの主な部分は、次のような型チェックです。関数の実際のパラメータと仮パラメータの型が一致するかどうか。弱い型付け言語の場合、このステップは利用できない可能性があります (エネルギーが限られており、JS エンジンの実装を調べる時間がないため、存在するかどうかはわかりません)。 JavaScript エンジンの
場合は、字句解析と構文解析が必要であり、これらのコンパイル ステップが完了した後に、セマンティック チェックやコードの最適化などのステップが必要になることがわかりました (どの言語でも同様です)。コンパイル プロセスですが、インタープリタ言語はバイナリ コードにコンパイルされません)、コードの実行が開始されます。
上記のコンパイル プロセスでは、記事の冒頭の「事前解析」についてはまだ説明できません。
Zhou Aimin 氏は、「JavaScript 言語の
本質」の中
で、これについて非常に注意深く分析しています。以下に私の洞察をいくつか示します。
JavaScript のスコープ
メカニズムを理解すると、JavaScript 変数のスコープは定義時に決定されます。つまり、字句スコープはソース コードに依存するため、静的解析を通じて字句スコープを決定することができます。ただし、字句スコープは静的スコープとも呼ばれます。実際、eval は静的テクノロジだけで実現することはできません。実際には、JS のスコープ メカニズムについてのみ説明できます。JS
エンジンは各関数インスタンスを実行するときに、実行コンテキストを作成します。 call オブジェクトは、varDecls、組み込み関数テーブル funDecls、親参照リスト upvalue などの内部変数テーブルを保存するために使用される scriptObject 構造体です (注: varDecls や funDecls などの情報は、実行中に取得されます)。関数インスタンスが実行されると、この情報は構文ツリーから scriptObject にコピーされます。この情報は、関数インスタンスのライフサイクルと一致する、関数に関連する静的なシステムです。 。
レキシカルスコープはJSのスコープの仕組みであり、その実装方法も理解する必要があります。これがスコープチェーンです。スコープ チェーンは、名前検索メカニズムです。まず、現在の実行環境で scriptObject を検索します。見つからない場合は、上位の scriptObject をたどって、グローバル オブジェクトを検索します。
関数インスタンスが実行されると、クロージャが作成されるか、クロージャに関連付けられます。 scriptObject は関数に関連する変数テーブルを静的に保存するために使用されますが、クロージャーはこれらの変数テーブルと実行中の値を動的に保存します。クロージャのライフサイクルは、関数インスタンスのライフサイクルよりも長くなる可能性があります。関数インスタンスはアクティブな参照が空になった後に自動的に破棄され、クロージャはデータ参照が空になった後に JS エンジンによってリサイクルされます (場合によっては自動的にリサイクルされず、メモリ リークが発生します)。
上記の一連の名詞に怯える必要はありません。実行環境、オブジェクトの呼び出し、クロージャ、字句スコープ、およびスコープ チェーンの概念を理解すれば、JS 言語の多くの現象を簡単に解決できます。
まとめ この時点で、記事の冒頭の質問は非常に明確に説明できます。
ステップ 3 のいわゆる「事前解析」は、実際にはステップ 2 の構文解析段階で完了し、構文ツリーに保存されます。関数インスタンスが実行されると、varDelcs と funcDecls が構文ツリーから実行環境の scriptObject にコピーされます。
ステップ 4 では、未定義の変数は scriptObject の変数テーブルで見つからないことを意味します。どちらも見つからない場合、書き込み操作は最終的に window と同等になります。 i = 1; 新しい属性をウィンドウ オブジェクトに追加します。読み取り操作の場合、グローバル実行環境までトレースバックされる scriptObject が見つからない場合、ランタイム エラーが発生します。
理解すると霧が晴れ、花が咲き、空が晴れてきました。
最後に質問を残しておきます:
<script type="text/javascript">
変数引数 = 1;
関数 foo(arg) {
アラート(引数);
変数引数 = 2;
}
foo(3);
</script>
アラートの出力は何ですか?