Get to know MDN better
このページはコミュニティーの尽力で英語から翻訳されました。MDN Web Docs コミュニティーについてもっと知り、仲間になるにはこちらから。
このページでは、JavaScript ランタイム環境の基本的な仕組みについて解説します。このモデルは主に理論的かつ抽象的なものであり、特定のプラットフォームや実装に関する詳細は含まれていません。現行の JavaScript エンジンは、ここで説明する意味づけを大幅に最適化しています。
このページはリファレンスです。読者が C や Java などの他のプログラミング言語の実行モデルについてすでに理解していることを想定しています。また、オペレーティングシステムやプログラミング言語における既存の概念を多用しています。
JavaScript を実行するには、JavaScript エンジンとホスト環境という 2 つのソフトウェアの連携が求められます。
JavaScript エンジンは、ECMAScript (JavaScript) 言語を実装し、中核となる機能を提供します。このエンジンはソースコードを受け取り、それを構文解析して実行します。しかし、意味のある出力を生成したり、外部リソースと連携したり、セキュリティやパフォーマンスに関連するメカニズムを実装したりするなど、外部とやり取りを行うためには、ホスト環境によって提供される環境固有の追加メカニズムが必要となります。例えば、ウェブブラウザー内での JavaScript の実行においては、HTML DOM がホスト環境となります。また、Node.js は、サーバーサイドで JavaScript を実行できるようにする別のホスト環境です。
このリファレンスでは主に ECMAScript で定義された仕組みに焦点を当てていますが、時折、HTML 仕様で定義された仕組みについても触れます。これらは Node.js や Deno といった他のホスト環境でも多くの場合採用されています。これにより、ウェブ上およびそれ以外の場面で使用される JavaScript の実行モデルについて、一貫した全体像をお伝えすることができます。
JavaScript 仕様書では、JavaScript のそれぞれの実行主体はエージェントと呼ばれ、コード実行のための機能を維持しています。
これらは、それぞれ異なるデータを管理する 3 つの異なるデータ構造です。キューとスタックについては、後の章で詳しく説明します。ヒープメモリーの割り当てと解放の詳細については、メモリー管理をご覧ください。
各エージェントはスレッドに相当します(なお、その実装の基盤となるものが実際のオペレーティングシステムのスレッドであるかどうかは場合によります)。各エージェントは、互いに同期的にアクセスできる複数のレルム(これらはグローバルオブジェクトと 1 対 1 で対応します)を所有することが可能で、そのため単一の実行スレッド内で実行される必要があります。また、エージェントには単一のメモリーモデルがあり、リトルエンディアンであるかどうか、同期的にブロックされるかどうか、原子操作がロックフリーであるかどうかなどが示されます。
ウェブ上のエージェントには、次のようなものがあります。
言い換えれば、それぞれのワーカーは独自のエージェントを生成しますが、1 つ以上のウィンドウが同じエージェント内に存在する場合があります。通常は、メイン文書と、それと同じオリジンを持つ iframe などが該当します。Node.js では、ワーカースレッドと呼ばれる同様の概念が利用可能です。
次の図は、エージェントの実行モデルを示しています。
それぞれのエージェントは、1 つ以上のレルム (realm) を所有しています。JavaScript コードは読み込まれた際に特定のレルムに関連付けられ、別のレルムから呼び出された場合でも、その関連付けは変わりません。レルムは以下の情報で構成されています。
ウェブ上では、レルムとグローバルオブジェクトは1対1で対応しています。グローバルオブジェクトは、Window、WorkerGlobalScope、WorkletGlobalScope のいずれかです。したがって、例えば、すべての iframe は、親ウィンドウと同じエージェント内にある場合でも、それぞれ異なるレルムで実行されます。
グローバルオブジェクトのアイデンティティについて言及する際、通常は「レルム」という言葉が登場します。例えば、Array.isArray() や Error.isError() といったメソッドが必要になるのは、別のレルムで構築された配列は、現在のレルムにある Array.prototype オブジェクトとは異なるプロトタイプオブジェクトを持つため、instanceof Array が誤って false を返すことになるからです。
まず、同期的なコードの実行について考えます。それぞれのジョブは、関連付けられたコールバックを呼び出すことで実行されます。このコールバック内のコードは、変数の生成、関数の呼び出し、終了を行うことがあります。各関数は、自身の変数環境と、どこに戻るべきかを追跡する必要があります。これを処理するために、エージェントは実行コンテキストを追跡するためのスタックを必要とします。実行コンテキスト(一般にスタックフレームとも呼ばれます)は、実行の最小単位です。これでは以下の情報が追跡されます。
次のコードで定義された単一のジョブからなるプログラムを想像してみてください。
フレームがポップされたとしても、必ずしも永久に消えてしまうわけではありません。なぜなら、後でそのフレームに戻る必要がある場合もあるからです。例えば、ジェネレーター関数を考えてみましょう。
この場合、gen() を呼び出すと、まず実行コンテキストが作成されますが、これは一時停止された状態になります。つまり、gen 内のコードはまだ実行されません。ジェネレーター g は、この実行コンテキストを内部的に保存します。現在実行中の実行コンテキストは、引き続きエントリーポイントとして残ります。g.next() が呼び出されると、gen の実行コンテキストがスタックにプッシュされ、gen 内のコードが yield 式まで実行されます。その後、ジェネレーターの実行コンテキストは一時停止され、スタックから除去されます。これにより、制御はエントリーポイントに戻ります。g.next() が再度呼び出されると、ジェネレーターの実行コンテキストがスタックに再びプッシュされ、gen 内のコードは中断した箇所から再開されます。
仕様書で定義されているメカニズムの一つに、プロパーテールコール (proper tail call, PTC) があります。関数呼び出しがテールコールであるとは、呼び出し側が呼び出し後に値を返す以外の処理を行わない場合を指します。
この場合、g への呼び出しはテールコールとなります。関数呼び出しが末尾の位置にある場合、エンジンは g() の呼び出し用に新しいフレームをスタックにプッシュするのではなく、現在の実行コンテキストを破棄し、それをテールコールのコンテキストに置き換える必要があります。つまり、テール再帰はスタックサイズの制限を受けません。
実際には、現在のフレームを破棄するとデバッグ上の問題が発生します。なぜなら、g() がエラーを発生させた場合、f はスタックに残らず、スタックトレースに現れなくなるからです。現在、PTC を実装しているのは Safari (JavaScriptCore) のみであり、デバッグ性の課題を解決するために、いくつかの固有の仕組みを構築しています。
変数のスコープと関数呼び出しに関連するもう一つの興味深い現象が、クロージャです。関数が生成されるたびに、その関数は内部的に、現在実行中の実行コンテキストにおける変数のバインドを記憶します。その結果、これらの変数のバインドは、実行コンテキストが終了した後も存続することがあります。
エージェントはスレッドであり、つまりインタープリタは一度に1つの文しか処理できません。コードがすべて同期的であれば、常に処理を進めることができるため、問題はありません。しかし、コードが非同期処理を実行する必要がある場合、その処理が完了しない限り、処理を進めることはできません。しかし、それによってプログラム全体が停止してしまうと、使い勝手に影響を及ぼしてしまいます。ウェブスクリプト言語としての JavaScript の性質上、ブロックしないことが求められるのです。そのため、その非同期処理の完了を処理するコードは、コールバックとして定義されます。このコールバックはジョブを定義し、処理が完了すると、そのジョブはジョブキュー(HTML の用語で言えばイベントループ)に入れられます。
エージェントは毎回、キューからジョブを取り出して実行します。ジョブが実行されると、同時に新しいジョブが生成され、それらはキューの末尾に追加されます。また、タイマー、I/O、イベントなどの非同期プラットフォームメカニズムの完了によっても、ジョブが追加されることがあります。ジョブは、スタックが空になった時点で完了したとみなされ、その後、キューから次のジョブが取り出されます。ジョブは一律の優先度で取り出されるとは限りません。例えば、HTML イベントループでは、ジョブをタスクとマイクロタスクの 2 つのカテゴリーに分類します。マイクロタスクは優先度高しで、タスクキューからジョブが取り出される前に、まずマイクロタスクキューが処理されます。情報については、HTML マイクロタスクガイドを参照してください。ジョブキューが空の場合、エージェントは新しいジョブを追加するのを待機します。
それぞれのジョブは、他のジョブが処理される前に完了します。これにより、プログラムを分析する際にいくつかのプロパティがあります。例えば、関数が実行されると、その実行は割り込みを受けることがなく、他のコードが実行される前に完全に完了します(また、その関数が操作するデータを変更することも可能です)。これは、例えば C 言語とは異なります。C 言語では、関数がスレッド内で実行されている場合、ランタイムシステムによっていつでも中断され、別のスレッド内のコードが実行されることがあります。
例えば、次のような例を考えてみましょう。
この例では、すでに解決済みのプロミスを作成しています。つまり、これに添付されたコールバックはすべて、直ちにジョブとしてスケジュールされます。2 つのコールバックは競合状態を発生させているように見えますが、実際には出力は完全に予測可能です。つまり、1 と 2 が順番にログ出力されます。これは、それぞれのジョブが完了してから次のジョブが実行されるためです。したがって、全体的な順序は常に i += 1; console.log(i); i += 1; console.log(i); となり、決して i += 1; i += 1; console.log(i); console.log(i); とはなりません。
このモデルの欠点は、ジョブの完了に時間がかかりすぎると、ウェブアプリケーションがクリックやスクロールといったユーザーの操作を処理できなくなることです。ブラウザーは、「スクリプトの実行に時間がかかりすぎます」というダイアログを表示することで、この問題を緩和します。推奨される対策としては、ジョブの処理時間を短くすること、そして可能であれば、1 つのジョブを複数のジョブに分割することです。
イベントループモデルが提供するもう一つの重要な保証は、JavaScript の実行が決してブロックされないということです。I/O の処理は通常、イベントとコールバックを通じて行われるため、アプリケーションが IndexedDB クエリーの返答や fetch() リクエストの返答を待っている間でも、ユーザー入力などの他の処理を継続することが可能です。非同期アクションの完了後に実行されるコードは、常にコールバック関数として指定されます(例えば、プロミスの then() ハンドラー、setTimeout() のコールバック関数、またはイベントハンドラーなど)。これらは、アクションが完了した際にジョブキューに追加されるタスクを定義するものです。
もちろん、「ブロックしない」という保証を実現するには、プラットフォームの API が本質的に非同期である必要がありますが、alert() や同期型の XHR など、一部の古い例外が存在します。アプリケーションの応答性を確実に実現するため、これらを避けることが推奨されています。
複数のエージェントはメモリー共有を通じて通信し、エージェントクラスターを形成することができます。エージェントは、メモリーを共有できる場合に限り、同じクラスター内に存在します。2 つのエージェントクラスター間で情報を交換するための組み込みの仕組みは存在しないため、これらは完全に独立した実行モデルと見なされます。
エージェントを作成する際(ワーカーの生成など)、それが現在のエージェントと同じクラスターに属するか、それとも新しいクラスターが作成されるかについては、いくつかの基準があります。例えば、以下のグローバルオブジェクトのペアは、それぞれ同じエージェントクラスター内に存在するため、互いにメモリーを共有することが可能です。
以下のグローバルオブジェクトのペアは、同じエージェントクラスター内に存在しないため、メモリーを共有することはできません。
正確なアルゴリズムについては、HTML 仕様書を調べてください。
前述の通り、エージェントはメモリー共有を通じて通信します。ウェブ上では、メモリーは postMessage() メソッドを介して共有されます。ウェブワーカーの使用方法ガイドでは、この概要が説明されています。通常、データは値渡しのみで(構造化複製を通じて)渡されるため、並行処理に関する複雑な問題は生じません。メモリーを共有するには、SharedArrayBuffer オブジェクトを投稿しなければならない。これにより、複数のエージェントが同時にアクセス可能になる。2 つのエージェントが SharedArrayBuffer を通じて同じメモリーへのアクセスを共有すると、Atomics オブジェクトを使用して実行を同期させることができる。
共有メモリーにアクセスするには、通常のメモリーアクセス(原子性を持ちません)と、原子的なメモリーアクセスの 2 つの方法があります。後者は逐次一貫性(つまり、クラスター内のすべてのエージェントが合意した厳密なイベントの全順序が存在することを意味します)を保証しますが、前者は順序付けられていません(つまり、順序が存在しないことを意味します)。JavaScript では、それ以外の順序保証を持つ操作は提供されていません。
この仕様書では、共有メモリーと一緒に作業するプログラマー向けに、以下のガイドラインを指定しています。
プログラムはデータ競合が発生しないようにすることを推奨します。つまり、同じメモリ位置に対して非原子的な操作が並行して行われることが絶対にないような構造にする必要があります。データ競合のないプログラムは、各エージェントの評価セマンティクスにおける各ステップが互いにインターリーブされるようなインターリーブセマンティクスを持っています。データ競合のないプログラムについては、メモリーモデルの詳細を理解する必要はありません。その詳細を知ったところで、ECMAScript をより良く記述するための直感が役立つことは難しいでしょう。
より一般的には、たとえプログラムにデータ競合が存在したとしても、データ競合に関与する操作に原子的操作が含まれておらず、かつ競合するすべての操作のアクセスサイズが同一である限り、予測可能な動作を持つことができる可能性があります。原子的操作が競合に関与しないようにする最も簡単な方法は、原子的操作と非原子的操作で異なるメモリセルを使用するようにし、異なるサイズの原子的アクセスが同時に同じセルにアクセスしないようにすることです。実質的に言えば、プログラムは共有メモリーを可能な限り厳密な型付けとして扱うべきです。競合する原子的でないアクセスの順序やタイミングに依存することは依然としてできませんが、メモリーを厳密な型付けとして扱うことで、競合するアクセスによる「ティアリング」(値のビットが混在すること)は発生しません。
複数のエージェントが連携する場合、ブロックしないという保証は常に成り立ちません。あるエージェントは、別のエージェントが何らかのアクションを実行するのを待っている間に、ブロック、つまり一時停止状態になることがあります。これは、同じエージェント内でプロミスの完了を待つ場合とは異なります。なぜなら、エージェント全体が停止し、その間、他のコードは一切実行できなくなるからです。言い換えれば、処理を先に進めることができないのです。
デッドロックを防ぐため、どのエージェントがいつブロックされるかについて、厳しい制限があります。
エージェントクラスターは、外部からの一時停止や終了が発生した場合でも、エージェントの稼働状況について一定の整合性を保証します。
| ECMAScript® 2027 Language Specification |
| ECMAScript® 2027 Language Specification |
| HTML |
より良いインターネットのための青写真。
このコンテンツの一部は、©1998–2026 個人の mozilla.org 協力者です。コンテンツはクリエイティブ・コモンズ・ライセンスのもとで利用できます。