1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaScript 実行モデル ~ JavaScript の非同期を理解する 第 2 弾

Last updated at Posted at 2025-06-10

JavaScript 実行モデル

JavaScript の非同期を理解する記事の第 2 弾です。
今回は、JavaScript の実行モデルの概要を理解します。

前回の記事: JavaScript の歴史と特徴

本記事は、以下の MDN の記事と ECMAScript の仕様を参照しています。

JavaScript technologies overview

ECMAScript® 2026 Language Specification

JavaScript の実行モデルの概要を理解してみよう

この記事シリーズは、最終的に JavaScript の非同期処理を理解することが目的ですが、本記事では非同期以前に、JavaScript の実行時に何が起こっているかを理解するための概念をまとめていきます。いきなり ECMAScript を読み始めると、その分量から途方に暮れてしまうため、ここではまず MDN の記事を読んで、そこから関連する ECMAScript を読むことで概念を理解していきます。

JavaScript execution model

また MDN の記事は日本語版もありますが、今回は英語版を参照してまとめています。

The engine and the host
JavaScript execution requires the cooperation of two pieces of software: the JavaScript engine and the host environment.
The JavaScript engine implements the ECMAScript (JavaScript) language, providing the core functionality. It takes source code, parses it, and executes it. However, in order to interact with the outside world, such as to produce any meaningful output, to interface with external resources, or to implement security- or performance-related mechanisms, we need additional environment-specific mechanisms provided by the host environment. For example, the HTML DOM is the host environment when JavaScript is executed in a web browser. Node.js is another host environment that allows JavaScript to be run on the server side.

まず、JavaScript の実行は、JavaScript エンジンとホスト環境の両方が必要です。これがそもそも他の高級言語と異なる点ではないでしょうか。
C#や Python などの高級言語は、ホスト環境自体も一体化されたランタイム(.NET や CPython など)が提供されており、言語解析、実行、メモリ管理などを一元的に実行が可能です。しかし一方で JavaScript は、言語解析やコンパイル、実行を行う、JavaScript エンジンと、アプリケーションとして必要なインターフェースを提供するホスト環境の 2 つが明確に分かれています。
JavaScript エンジンは ECMAScript の仕様を実装する一方で、ブラウザでいう HTML DOM のようなホスト環境がないと JavaScript は実行できません。

またその下には、"agent execution model" という概念があります。

agent execution model

agent execution model は、ざっくりというと JavaScript のホスト環境を定義する概念であり、JavaScript がコード評価、メモリ管理、イベント処理を行う実行単位を表します。
ざっくりとした例えでは、Web ブラウザのタブ単位や Node.js のプロセス単位などがこれに当たります。
他の言語でのスレッドと似ていますが、JavaScript はスレッドという概念を持っておらず、別の方法で、メモリ共有やアトミック操作を実現しています。

また、agent は 3 つの特徴的な、要素からなります。それが Heap, Queue, Stack です。

Heap

Heap は、動的メモリ割り当ての領域です。JavaScript のオブジェクトや関数はヒープ上に各校され、そのアドレスを参照することでアクセスできます。

Queue (Job Queue)

Queue は、コードの実行順序を管理するキューです。JavaScript のコードは、このキューに順番に追加され、実行されます。
ただし Queue は ECMAScript の仕様とホスト環境(ブラウザや Node.js)の仕様が密接に連携して機能しているためホスト環境の仕様との関係性を踏まえて、次回記事にて詳細を解説します。

Stack(Execution Context Stack)

Stack は、JavaScript の関数呼び出しおよび実行コンテキスト(Execution Context)の制御を行う LIFO(後入れ先出し)構造の記憶領域です。この Stack は、ECMAScript 用語では Execution Context Stack と呼ばれ、実行コンテキストの push/pop によって評価順序が決まります。JavaScript は基本的にシングルスレッドですが、これは「一つの Stack を持つ一つの agent でコードが評価される」ことを意味します。並列性は Web Workers や agent cluster のようなホスト環境のサポートに委ねられます。
余談ですが無限ループの JavaScript を実行すると、Stack overflow が発生しますが、それはこの Stack が溢れたことを意味しています。

この辺りで少しわかりづらくなってきたかと思います。よくわからないと思った方は、このサイトを見てみてください。これは、JavaScript の実行モデルを可視化したサイトです。いくつかの典型的な例も用意されており、実際の実行順を追って理解することができるようになっています。

JavaScript Visualizer 9000

Realm (レルム)

Realm は ECMAScript で定義されているホスト環境の概念です。
これも他の言語ではあまり聞かない概念ですが、その目的は「実行空間の分離」です。
元々 JavaScript はスクリプト言語として、Web ブラウザで動作することを前提に作られています。Web ブラウザではさまざまなサービスが動いており、各ホスト環境を分ける境界が必要になります。同じ Agent 内のオブジェクトはお互いにアクセスが可能ですが、同一 Agent 内でも Realm が異なるとアクセスできないようになっています。

例えば、一般的に Web ブラウザの iframe 同士は互いにアクセスしてオブジェクト取得をすることができません。これは同じ Agent の中でも Realm が異なるためです。

実行コンテキスト(Execution Context)

JavaScript が評価・実行される際、そのスコープやバインディング、実行状態を記録するために、Execution Context が作成されます。
Execution Context は上述した、Stack に push, pop され JavaScript のコードの実行単位となります。

  • Variable Environment
    • var 変数や関数宣言は「宣言時」に環境レコードへ登録されます。古い概念であり、モダンな JavaScript では使用しないようになっています。
  • Lexical Environment
    • 変数や関数宣言は「宣言時」に環境レコードへ登録されます。let, const はこの環境レコードに登録されます。
  • This Binding
    • 現在のコンテキストでの this の指すオブジェクトを管理します。
  • ScriptOrModule
    • 実行されているコードがモジュールかスクリプトかを管理します。

まとめ

  • JavaScript の実行モデルは agent を基本単位とし、その中で Heap, Queue, Stack によって評価と実行が行われます。
  • Realm によって実行空間が分離されることで、1 つの agent 内で複数の JavaScript が実行されていても違いのオブジェクトへのアクセスを防ぐことができます。
  • コードの実行単位は Execution Context によって記録され、Stack に push/pop されることで処理されます。

ECMAScript を少し読んでみよう

さてここまで色々な単語が出てきましたが、ここからは ECMAScript の仕様も少しだけ読んでみようと思います。

まず Overview の章には以下のようにあります。

This section contains a non-normative overview of the ECMAScript language.

ECMAScript is an object-oriented programming language for performing computations and manipulating computational objects within a host environment. ECMAScript as defined here is not intended to be computationally self-sufficient; indeed, there are no provisions in this specification for input of external data or output of computed results. Instead, it is expected that the computational environment of an ECMAScript program will provide not only the objects and other facilities described in this specification but also certain environment-specific objects, whose description and behaviour are beyond the scope of this specification except to indicate that they may provide certain properties that can be accessed and certain functions that can be called from an ECMAScript program.

ECMAScript was originally designed to be used as a scripting language, but has become widely used as a general-purpose programming language. A scripting language is a programming language that is used to manipulate, customize, and automate the facilities of an existing system. In such systems, useful functionality is already available through a user interface, and the scripting language is a mechanism for exposing that functionality to program control. In this way, the existing system is said to provide a host environment of objects and facilities, which completes the capabilities of the scripting language. A scripting language is intended for use by both professional and non-professional programmers.

ECMAScript was originally designed to be a Web scripting language, providing a mechanism to enliven Web pages in browsers and to perform server computation as part of a Web-based client-server architecture. ECMAScript is now used to provide core scripting capabilities for a variety of host environments. Therefore the core language is specified in this document apart from any particular host environment.

ECMAScript usage has moved beyond simple scripting and it is now used for the full spectrum of programming tasks in many different environments and scales. As the usage of ECMAScript has expanded, so have the features and facilities it provides. ECMAScript is now a fully featured general-purpose programming language.

ECMAScript® 2026 Language Specification 2025 年 4 月 10 日時点

ここでは全体概要として、言語の位置づけを記載してくれています。これによると

  • ECMAScript はホスト環境に依存するオブジェクト指向プログラミング言語であり、自己完結するホスト環境を持たない。
  • そのため、ECMAScript プログラムは、ECMAScript プログラムを実行するホスト環境によって提供されるオブジェクトと機能を利用する。(例えばブラウザが提供する API など)
  • 元々は Web ブラウザスクリプト言語として開発されたが、現在では、多岐に渡るホスト環境で利用されており、汎用的なプログラミング言語として発展している。

となっており、そもそもの JavaScript の言語としての立ち位置と現在の位置づけがわかります。

また上述した実行モデルの概念は以下の章に記載されています。

ECMAScript® 2026 Language Specification: 9 Executable Code and Execution Contexts 2025 年 4 月 10 日時点

例えば、Realm は以下のように定義されています。

9.3 Realms
Before it is evaluated, all ECMAScript code must be associated with a realm. Conceptually, a realm consists of a set of intrinsic objects, an ECMAScript global environment, all of the ECMAScript code that is loaded within the scope of that global environment, and other associated state and resources.

A realm is represented in this specification as a Realm Record with the fields specified in Table 24:

これは、すべての ECMAScript コード(JavaScript)は必ず Realm に関連付けられる必要があるということ。概念的には、Realm は一連の組み込みオブジェクト、ECMAScript グローバル環境、そのグローバル環境のスコープ内でロードされたすべての ECMAScript コード、その他の関連する状態とリソースから成る概念ということがわかります。

少し漠然としていると感じるかもしれませんが、ECMAScript の仕様はこのように、抽象化された概念を定めていることが多く、具体的な実装は JavaScript エンジンとホスト環境に任せていることが多いです。分量はかなり多いですが、興味ある部分を読んでみることをお勧めします。本記事では、Jobs の実行についての部分を読んでみることで、JavaScript の実行概念を理解し、非同期処理の理解へと繋げていきます。

JavaScript の "Job"の概念を理解してみよう

9.5 Jobs and Host Operations to Enqueue Jobs

ここで言われている "Job" は、JavaSCript 実行の概念単位です。JavaScript を実行する際には、"Jobs"をスケジューリングして実行します。当たり前のことを言っているように感じるかもしれませんが、これは JavaScript の実行モデルの中では非常に重要な概念です。そしてこの中で、agent 内では常に 1 つの Job しか実行されないという定義がされています。

あれ?だとするとどうやって非同期処理を実現しているの?という疑問が湧いてくるかと思います。
そして読み進めていくと、以下のような記載があります。

Note 1
Host environments are not required to treat Jobs uniformly with respect to scheduling. For example, web browsers and Node.js treat Promise-handling Jobs as a higher priority than other work; future features may add Jobs that are not treated at such a high priority.

訳すと "ホスト環境は Job を均一に扱う必要はなく、例えばブラウザや Node.js は Promise-handling Jobs を他の作業よりも優先して扱っている。" となります。
これは、ホスト環境側で各 "Job"の実行優先順位を決めることができるということです。つまり一度に 1 つの"Job"しか実行できないが、どの"Job"を実行するかはホスト環境側で決めることができるということです。

そして、"Job"のスケジューリングとして、以下のような処理を用意するよう定義されています。

9.5.4 HostEnqueueGenericJob ( job, realm )

これは"Job"をキューに追加するための関数です。引数のrealmによって決まるagent内で、将来のある時刻に実行するように"Job"をスケジュールすることができます。 またこの"Job"は優先度や順序のような追加の制約がないため、ホスト環境側で自由に制御できることになります。

9.5.5 HostEnqueueJob ( job, realm )

これも同じく、将来のある時刻に実行するように"Job"をスケジュールする関数です。しかし大きな違いとして、Promise オブジェクトを扱うことを意図して存在しています。そのため Promise オブジェクトと同じ優先度で実行されることになります。

ここまでで、JavaScript は"Job"という概念単位で実行されていることがわかりました。そして "Job"は基本的にホスト側で優先順位などの制御が可能だが、一部の"Job"はPromiseとして優先度を高くスケジュールすることができる、ということもわかりました。ここまで読むといくつかの疑問が湧いてくるかと思います。

  1. agent 内では常に 1 つの Job しか実行されない。では、どうやって非同期を実現しているの?
  2. ホスト環境が Job の優先順位を決めることはできるというが、"Job"は Queue に入っているため、FIFO で実行順序が決まっているのでは?
  3. そもそも Promise はどうやって優先度を高くスケジュールするの?

先に言ってしまうと、これらは ECMAScript で具体的に示されていません。ECMAScript はあくまで定義をしており、具体的にどのように実現するかは、ホスト環境、そして実際の実装に任されています。
次回の記事ではブラウザ側の仕様を読み解いていきますが、先に上記の疑問に答えると

  1. これまで言ってきたように、非同期処理は、マルチスレッドなどにみられる並列処理ではなく、ホスト環境の API などを用いて実現する並行処理です。非同期リクエストは、API などにより別環境で実行され、Callback 関数が "Job"としてキューに追加されます。そのため、並列的に実行しているように見えます。
  2. ホスト環境は、複数のキューを持っています。またキューという名称ですが、実態は "set"型のデータ構造であり、実行時に取得する"Job" を選択することが可能となっています。
  3. 2 の回答と関連しますが、ホスト環境は、Promise オブジェクトを扱うためのキューを持っています。(これは"Queue"構造です。なぜなら、'Promise'同士には優先順位がなく、実行順に処理される必要があるためです。)

しかし、重ねますが、これらは ECMAScript だけを読んでもわかりません。ホスト環境の仕様と合わせて読んでいくことで、理解が可能となります。
次回記事では、ブラウザの仕様を読み解いていきますが、その前に、本記事の内容を手元で試してみることにします。

コードを実行してみよう

ここからは実際に JavaScript を動かしてみることにします。自分は以下のツールを使用しました。

JavaScript エンジンのバージョンを管理するツール
jsvu is the JavaScript (engine) Version Updater
https://github.com/GoogleChromeLabs/jsvu?tab=readme-ov-file

V8 は Google Chrome や Node.js の OSS の JavaScript エンジン。
v8 を利用することで、ホスト環境(ブラウザなど)に関係のない、JavaScript の振る舞いを確認することができる。
( console.log はホスト環境依存の処理だが、v8 では実装されている。)

参考: V8 のブログ
https://v8.dev/blog

Stack overflow を試してみる

function recurse() {
  return recurse();
}

recurse();

上記をブラウザの inspector などで試すと、Stack overflow となります。

サンプルコードで試してみる - グローバル実行コンテキスト

  1. デバッグしたいファイルを実行
// test_global_context.js

global.globalVar = "Hello, Global!";
var nonGlobalVar = "This is wrapped by Node";
const constVar = "Hello, Const!";
let letVar = "Hello, Let!";

console.log(globalVar);
console.log(nonGlobalVar);
console.log(constVar);
console.log(letVar);

上記を保存し以下コマンドで実行

node --inspect-brk v8/test_global_context.js
  1. Chrome ブラウザを開く
  2. アドレスバーに chrome://inspect と入力
  3. 「Remote Target」セクションに表示される inspector をクリック
  4. 「DevTools」が開き、コードの実行を制御できます

そうすると Sources タブで実際にデバッグしながら実行できます。右側に "Scope"があり、そこで、Global, Local にそれぞれの変数が確認できます。
これはイメージだと以下のような、Execution Context の構造になっています。

ここで、var, let, const での宣言は Function Execution Context に含まれていますが、これは Node.js が実行時に Wrap しているためです。ブラウザで実行すると、"globalVar" だけでなく "nonGlobalVar" も Global Execution Context に含まれることになります。

debugger.png

Global Execution Context:
└─ Global Lexical Environment
   ├─ EnvironmentRecord:
   │   └─ globalVar: ポインタ → "Hello, Global!"(ヒープ上)
   └─ Outer: null(最上位)

Function Execution Context (Node.js の IIFE):
└─ Function Lexical Environment
   ├─ EnvironmentRecord:
   │   ├─ nonGlobalVar: ポインタ → "This is wrapped by Node"(ヒープ上)
   │   ├─ constVar: ポインタ → "Hello, Const!"(ヒープ上)
   │   └─ letVar: ポインタ → "Hello, Let!"(ヒープ上)
   └─ Outer: Global Lexical Environment

サンプルコードで試してみる - Call Stack

// test_call_stack.js
function inspectCallStack() {
  console.log(new Error().stack);
}

function factorial(n) {
  inspectCallStack();

  if (n <= 1) return 1;
  return n * factorial(n - 1);
}

factorial(3);

上記を保存し以下コマンドで実行

node --inspect-brk v8/test_call_stack.js

デバッグで試してみると、同じく stack に関数が積まれていくことがわかるかと思います。
call_stack.png

サンプルコードで試してみる - Realm

// test_realm.js
const vm = require("vm");

// グローバルの Array インスタンスを作成
const arr1 = [];

console.log("arr1 instanceof Array:", arr1 instanceof Array); // true

// vmコンテキストで新しい Realm を作成
const context = vm.createContext();
vm.runInContext("arr2 = []", context); // arr2 を新しい Realm 上で作成

// 親 Realm の Array と比較してみる
console.log("arr2 instanceof Array:", context.arr2 instanceof Array); // false
console.log("arr2.constructor === Array:", context.arr2.constructor === Array); // false

上記を保存し以下コマンドで実行

node --inspect-brk v8/test_realm.js

このコードは、vm モジュールを使用して新しい Realm を作成し、その Realm 上で配列を作成しています。
そして、異なる Realm のオブジェクト同士を instanceof で比較しています。

このコードを実行すると、以下のような結果が出力されます。

arr1 instanceof Array: true
arr2 instanceof Array: false
arr2.constructor === Array: false

これはなぜでしょうか。

Before it is evaluated, all ECMAScript code must be associated with a realm. Conceptually, a realm consists of a set of intrinsic objects, an ECMAScript global environment, all of the ECMAScript code that is loaded within the scope of that global environment, and other associated state and resources.
https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html

ECMAScript の Web サイトによると、Realm は、一連の組み込みオブジェクト、ECMAScript グローバル環境、そのグローバル環境のスコープ内でロードされたすべての ECMAScript コード、その他の関連する状態とリソースから成る概念ということです。
つまり、上記のコードにおいては 「Array コンストラクタ自体が異なる Realm の中で定義された別インスタンスだから」です。
(instanceof は、オブジェクトが特定のコンストラクタのインスタンスとチェーンしているかどうかを判定しています。)

まとめ

この記事では、JavaScript の実行時に何が起こっているかを理解するための概念をまとめていきました。新しい概念が多く出てきましたが、ここまででも JavaScript が他の高級言語とはかなり異なった概念や実行モデルを元に作られていることがわかりました。

  • JavaScript はスクリプト言語として、ブラウザでの単一スレッド実行を目的として当初作成されています。そのため、他言語ではあまり聞かない概念が多くあります。
  • JavaScript は Stack, Queue といった概念を元に、コードの実行を管理しています。
  • 仕様は、ホスト環境と密接に関わっているため、ホスト環境の仕様を読み解くことが必要です。

次回

この記事では JavaScript の実行モデルについて解説しました。
次回は、ブラウザ側から見た JavaScript の実行モデルについてまとめていきます。
具体的には 'HTML Living Standard' の仕様を読み解き、'Event loop' や 'Task Queue' についてまとめていきます。("Job" は ECMAScript の仕様で定義されている概念で、"Task" はブラウザの仕様で定義されている概念です。同一と考えて差し支えないです。)

  • JavaScript がブラウザでどのように実行されているのか

そんな疑問に答える第3章は ブラウザでの JavaScript の実行 です。

(*この記事は、JavaScript について勉強した内容をまとめたものであり、内容が不正確な可能性があります。もし指摘などあれば、コメントいただけるととても嬉しいです。)

参考資料

What the heck is the event loop anyway? | Philip Roberts | JSConf EU

JavaScript execution model

ECMAScript® 262 Language Specification

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?