はじめに
近年 JavaScript の需要は増し、Web Application のほとんどは JavaScript を使って動いているのではないかと思います。
もともと JavaScript は DOM API (Document Object Model、HTML を JavaScript でから操作できる) の為にありました。DOM API を更に使いやすくした jQuery 等が流行りましたね。
しかし JavaScript の活用範囲は拡大し、現在では以下の様々な用途で利用されます。
- フロントエンド JS 開発の環境 (Babel, Webpack)
- Web バックエンド ランタイムサーバー (Express)
- クロスプラットフォームなデスクトップアプリケーションランタイム (Electron)
- AWS Lambda 等のサーバーレス環境のランタイム
例えばフロントエンド開発をするとき、ローカルマシンに Node.js をインストールして使いますが、Node.js では使えるがブラウザ JavaScript では使えない API (ファイル入出力等) があります。同じ JavaScript の筈なのに何故でしょう?
つまり、どこまでが JavaScript なのでしょうか?
巷でよく聞く ECMAScript (≒ JavaScript ?) とは何なのかを追っていきましょう。
Web ブラウザの仕組み (標準仕様と実装)
まずは JavaScript 実行のベースとなっている Web ブラウザの仕組みについて追っていきましょう。
構成要素
Web ブラウザの仕組みを図に書き出してみると、おおよそ以下の様になります。
以下は Goole Chrome での例です。
他ブラウザでも基本的なアーキテクチャはほぼ同じですが、一部異なる箇所はあります。
以下は図中の構成要素の説明です。
# | 構成要素 | 実装系 | 説明 |
---|---|---|---|
1 | ユーザーインターフェース | Google Chrome | ウインドウやタブ、URL バー等、デスクトップアプリケーションとしての UI 部分です。 (つまり OS 毎に実装が異なります) |
2 | ブラウザ エンジン | Google Chrome | UI 部分 (OS 上のプロセス) と、レンダリングエンジン間の橋渡しをします。 |
3 | レンダリング エンジン | Blink | 主に HTML の画面描画をしますが、JavaScript エンジンの初期化・起動等、様々な処理をします。 |
4 | JavaScript エンジン | V8 | ECMAScript (ECMA-262) 仕様に準拠したエンジンで、JavaScript コンテキスト (実行空間) の生成と、ユーザー JavaScript コードの実行をします。 |
(1) ユーザーインターフェース
ウインドウやメニュー等、アプリケーション (プロセス) としての UI 部分です。
この部分の実装は OS によって異なります。
(2) ブラウザ エンジン
ブラウザエンジンは、UI 部分 (メインプロセス) と、レンダラプロセスとの橋渡しを行います。
Google Chrome では、ウィンドウやタブを開く度にレンダリング処理をするサブプロセスが立ち上がります。
Chronium 系ブラウザではマルチプロセス動作をしますが、他ブラウザはそうではありません。
(3) レンダリング エンジン
Google Chrome では、レンダリングエンジンに Blink を採用しています。
レンダリングエンジンは主に HTML/CSS の描画処理をします。 (本書では描画については述べません)
後述する「JavaScript エンジン」への実行指示もレンダリングエンジンが行います。
(4) JavaScript エンジン
※次章で詳しく説明をします。
JavaScript コンテキストと Web API
ブラウザのウインドウやタブでページ遷移 (URL の変更) が発生すると、レンダリングエンジンから JavaScript エンジンへ 新しい JavaScript コンテキストの生成 が指示されます。
HTML 中に含まれる <script>
タグで入力したユーザー JavaScript は、この生成されたコンテキスト上に取り込まれ実行されます。
図解
# | 処理 | 説明 |
---|---|---|
1 | JavaScript エンジン (V8) は、新しいコンテキスト (JS 実行空間) を生成します | JavaScript の標準的な構文、オブジェクトが利用可能になります |
2 | Web IDL で定義された Web API がコンテキスト内に JavaScript オブジェクトとして定義される | レンダリングエンジンが提供する Web API が利用可能になります |
3 | ユーザー JavaScript コードを、コンテキスト内で実行する |
V8 は ECMASCRIPT に準拠
ここで最も重要な事は JavaScript エンジン (V8) は ECMASCRIPT (ECMA-262) 仕様の実装系である という事です。
例えば、ブラウザ JavaScript で良く使われる setTimeout() や console.log() は、実は ECMASCRIPT 仕様ではありません。
これらの API 実装はブラウザが提供しており、 Web API 仕様に当たります。
JS 仕様 | JavaScript 構文/API | 例 | |
---|---|---|---|
JavaScript (ECMASCRIPT) | 標準組込みオブジェクト |
Array , Number , Promise , JSON
|
|
式と演算子 |
+ , - , this , function
|
||
文と宣言 |
if...else , for...in
|
||
Web API | Console API | console.log() |
|
Document | Document.getElementById() |
↑ に挙げたのは一例で、網羅はしてません。
API がどの仕様に準拠しているか?
MDN のパンくずリストを見ると、その API が何の仕様に基づいているか分かります。
例えば Promise
は JavaScript (ECMAScript) 仕様です。
関数 setTimeout()
は Web API 仕様であることが分かります。
他ブラウザでは?
前章では Google Chrome を例に説明をしましたが、他ブラウザでも基本的な仕組みは同じで、実装系 (Blink や V8 等) が以下の通り異なります。
ブラウザ | レンダリングエンジン | JavaScript エンジン | 補足説明 |
---|---|---|---|
Google Chrome | Blink | V8 | Chromium ベース |
Mozilla FireFox | Gecko | SpiderMonkey | Mozilla がメインでメンテ |
Safari | WebKit | JavaScriptCore | Apple がメインでメンテ |
Microsoft Edge | Blink | V8 | Chromium ベースに移行 |
Microsoft Edge ※旧版 | EdgeHTML | Chakra | 2021年3月9日にサポートを終了 |
Internet Explorer | Trident | Chakra | 2022年6月16日にサポートを終了 |
モバイルもありますが、割愛。
Node.js の仕組み (標準仕様と実装)
ここまでで Web ブラウザの仕組みを追ってみました。
では Node.js はどういう仕組になっているのでしょうか?
Web ブラウザの仕組みを知っている事が Node.js の理解にも繋がります。
図解
# | 処理 | 説明 |
---|---|---|
1 | JavaScript エンジン (V8) は、新しいコンテキスト (JS 実行空間) を生成します | JavaScript の標準的な構文、オブジェクトが利用可能になります |
2 | Node.js 独自のコアモジュール (Node.js API) をコンテキスト中に定義する | コアモジュール (JavaScript) コードは Node.js リポジトリの lib/ 配下にあります。 実際の API 処理の実態は Node.js の C++ コード側にあり、lib/ 配下の JavaScript コードは該当する C++ 実装の Binding を取得してコード呼び出しをしています。 |
3 | ユーザー JavaScript コードを、コンテキスト内で実行する |
Node.js も Chrome と同じ V8 エンジン
Node.js は JavaScript エンジンに V8 を採用しています。従って標準的な JavaScript 仕様 (ECMASCRIPT) の範囲では Google Chrome と同じ様に動作します。
しかし Web ブラウザの章で述べた通り setTimeout() や console.log() の API は ECMASCRIPT 仕様に含まれていませんが、 Node.js ではこれらの API を問題なく使えます。何故でしょうか?
その答えは Node.js 独自の Node.js API (コアモジュール) として、これらの API が実装されているからです。
- Node.js v14.18.2 - setTimeout(callback[, delay[, ...args]])
- Node.js v14.18.2 - console.log([data][, ...args])
つまり上記の API は Web ブラウザでの Web API 仕様との互換性を保ちために追加されたものですが、 あくまで Node.js 独自の API であり、Web API 仕様を準拠しているわけではありません。
アップデートによる影響
Node.js をアップデートする際は前述の構造から次の2点が影響範囲であると分かります。
- JavaScript エンジン V8 の変更
- Node.js (コアモジュール) の変更
ECMAScript 仕様は後方互換性を最大限尊重します。
従って、JavaScript エンジン (V8) の変更による既存のコードへの影響は比較的少ないと言えます。
例えば Chrome 等の Web ブラウザはバックグラウンドで秘密裏にアップデートされています。
一方で、Node.js (コアモジュール) の変更では、破壊的変更が加えられる事は珍しくありません。
実際に Node.js のコードを追ってみよう!
ここからはかなりニッチな内容になるので興味ある方だけ ^^;
ユーザー JavaScript コードから fs.chmod() を呼んだ場合に、この API の実態 (C++ 実装) は Node.js ソースコード の何処にあるのか?を追います。
お題 → fs.chmod()
Node.js 公式の API Documentation に fs.chmod()
の記載があります。
- Node.js v14.18.2 - fs.chmod()
これをお題にコードを追いましょう。
import { chmod } from 'fs';
chmod('my_file.txt', 0o775, (err) => {
if (err) throw err;
console.log('Node.js 公式の chmod 使用例です.');
});
fs モジュールのフロント実装.
ユーザースクリプトで import { chmod } from 'fs';
しているモジュールの実態は lib/fs.js
にあります。
この function の JS 実装はたった6行しか無く、最終行 L1315 の binding.chmod()
が C++ 側の実装を呼んでいる箇所になります。
function chmod(path, mode, callback) {
path = getValidatedPath(path);
mode = parseFileMode(mode, 'mode');
callback = makeCallback(callback);
const req = new FSReqCallback();
req.oncomplete = callback;
binding.chmod(pathModule.toNamespacedPath(path), mode, req); // ← ここがキモ
}
この binding オブジェクトはファイルの先頭 L59 で生成されています。
const binding = internalBinding('fs');
JavaScript → C++ 実装への Binding の仕組み.
Node.js の JavaScript / C++ Binding の仕組み自体は README.md
に書いてあります。
書いてある事は難しいが、要は ↓ という事です。
C++ 側で
NODE_MODULE_CONTEXT_AWARE_INTERNAL(モジュール名, Initialize関数)
定義したモジュールは、JavaScript 側でinternalBinding('モジュール名')
で取得できるよ
実際に lib/fs.js
JavaScript モジュールに該当する C++ 実装は src/node_file.cc
ファイルになります。
C++ src/node_file.cc
ファイルの最終行 L2535 にバインディングの定義があります。
NODE_MODULE_CONTEXT_AWARE_INTERNAL(fs, node::fs::Initialize)
JavaScript fs.chmod()
に該当する C++ 実装は、このコード中の以下の関数 L2114 です。
/* fs.chmod(path, mode);
* Wrapper for chmod(1) / EIO_CHMOD
*/
static void Chmod(const FunctionCallbackInfo<Value>& args) {
// 省略...
}
JavaScript API 名 chmod
とこの C++ 関数 Chmod
との紐付けは同ファイルの Initialize()
関数内 L2439 にあります。
env->SetMethod(target, "chmod", Chmod);
env->SetMethod(target, "fchmod", FChmod);
env->SetMethod(target, "chown", Chown);
env->SetMethod(target, "fchown", FChown);
env->SetMethod(target, "lchown", LChown);