ブラウザ JavaScript / Node.js の仕組みを知ろう！ ～トラブルに迅速に立ち向かえる様に

## はじめに

近年 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 での例です。
> 他ブラウザでも基本的なアーキテクチャはほぼ同じですが、一部異なる箇所はあります。

![ブラウザ JavaScript _ Node.js の仕組みを知ろう！.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/362d61b5-6093-74ba-4348-375d02fabc62.png)

以下は図中の構成要素の説明です。

| \# | 構成要素 | 実装系 | 説明 |
|:----|:----|:----|:----|
| 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 によって異なります。

![Screen Shot 2021-12-19 at 17.01.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/6459e7fd-6068-058a-db25-cb4f20fdf74f.png)

![Screen Shot 2021-12-19 at 16.43.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/4b8f5c88-59a7-fe7e-c65c-b9eeea4ff040.png)


#### (2) ブラウザ エンジン

ブラウザエンジンは、UI 部分 (メインプロセス) と、レンダラプロセスとの橋渡しを行います。

![ブラウザ JavaScript _ Node.js の仕組みを知ろう！-ブラウザ-レンダラ.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/fdf47c0e-9ad2-4481-c39f-14cd9e57cf60.png)

Google Chrome では、ウィンドウやタブを開く度にレンダリング処理をするサブプロセスが立ち上がります。

![Screen Shot 2021-12-19 at 16.53.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/68261518-1500-b5c5-4d41-6543c32fe751.png)

> Chronium 系ブラウザではマルチプロセス動作をしますが、他ブラウザはそうではありません。


#### (3) レンダリング エンジン

[Google Chrome] では、レンダリングエンジンに [Blink] を採用しています。

> [Blink は、Safari 等で使われている WebKit の派生プロジェクトです。](https://ja.wikipedia.org/wiki/Blink_(%E3%83%AC%E3%83%B3%E3%83%80%E3%83%AA%E3%83%B3%E3%82%B0%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%B3))

レンダリングエンジンは主に HTML/CSS の描画処理をします。 (本書では描画については述べません)
後述する「JavaScript エンジン」への実行指示もレンダリングエンジンが行います。

#### (4) JavaScript エンジン

※次章で詳しく説明をします。


## JavaScript コンテキストと Web API

ブラウザのウインドウやタブでページ遷移 (URL の変更) が発生すると、レンダリングエンジンから JavaScript エンジンへ **新しい JavaScript コンテキストの生成** が指示されます。

HTML 中に含まれる `<script>` タグで入力したユーザー JavaScript は、この生成されたコンテキスト上に取り込まれ実行されます。

### 図解

![ブラウザ JavaScript _ Node.js の仕組みを知ろう！-JS と Web API.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/b868106a-1bdf-5901-af9f-70e2a8f27e58.png)

| \# | 処理 | 説明 |
|:----|:----|:----|
| 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)] | [標準組込みオブジェクト](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects) | `Array`, `Number`, `Promise`, `JSON` | |
| | [式と演算子](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators) | `+`, `-`, `this`, `function` | |
| | [文と宣言](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements) | `if...else`, `for...in` | |
| [Web API] | [Console API](https://developer.mozilla.org/ja/docs/Web/API/Console_API) | `console.log()` | |
| | [Document](https://developer.mozilla.org/ja/docs/Web/API/Document) | `Document.getElementById()` | |

> ↑ に挙げたのは一例で、網羅はしてません。

### API がどの仕様に準拠しているか？

[MDN] のパンくずリストを見ると、その API が何の仕様に基づいているか分かります。
例えば `Promise` は JavaScript ([ECMAScript]) 仕様です。

![Screen Shot 2021-12-19 at 17.59.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/87c29608-ed32-2461-e799-c37ffc50cc9e.png)

関数 `setTimeout()` は [Web API] 仕様であることが分かります。

![Screen Shot 2021-12-19 at 18.00.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/e4212c83-e482-77a4-4fc6-14a636429710.png)


## 他ブラウザでは？

前章では [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] の理解にも繋がります。

### 図解

![ブラウザ JavaScript _ Node.js の仕組みを知ろう！-Node.js.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/e550b16c-a5f6-e0a3-bf61-fb49be79bb5a.png)

| \# | 処理 | 説明 |
|:----|:----|:----|
| 1 | JavaScript エンジン (V8) は、新しいコンテキスト (JS 実行空間) を生成します | JavaScript の標準的な構文、オブジェクトが利用可能になります |
| 2 | Node.js 独自のコアモジュール ([Node.js API]) をコンテキスト中に定義する | コアモジュール (JavaScript) コードは Node.js リポジトリの [lib/] 配下にあります。<br/><br/>実際の 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]])](https://nodejs.org/dist/latest-v14.x/docs/api/timers.html#timers_settimeout_callback_delay_args)
* [Node.js v14.18.2 - console.log([data][, ...args])](https://nodejs.org/dist/latest-v14.x/docs/api/console.html#console_console_log_data_args)

つまり上記の API は Web ブラウザでの [Web API] 仕様との互換性を保ちために追加されたものですが、 **あくまで [Node.js] 独自の API であり、[Web API] 仕様を準拠しているわけではありません。**

> *[setTimeout() を使用して、 指定したミリ秒後に コードの実行をスケジュールすることができます。 この関数はブラウザの JavaScript API の window.setTimeout()に似ていますが、 コードの文字列を渡して実行することはできません。](https://nodejs.org/ja/docs/guides/timers-in-node/)*

### アップデートによる影響

Node.js をアップデートする際は前述の構造から次の2点が影響範囲であると分かります。

1. JavaScript エンジン [V8] の変更
1. Node.js (コアモジュール) の変更

[ECMAScript] 仕様は後方互換性を最大限尊重します。
従って、JavaScript エンジン ([V8]) の変更による既存のコードへの影響は比較的少ないと言えます。

> 例えば Chrome 等の Web ブラウザはバックグラウンドで秘密裏にアップデートされています。

一方で、Node.js (コアモジュール) の変更では、破壊的変更が加えられる事は珍しくありません。

* [Node.js v14.18.2 - Deprecated APIs](https://nodejs.org/dist/latest-v14.x/docs/api/deprecations.html)

## 実際に Node.js のコードを追ってみよう！

ここからはかなりニッチな内容になるので興味ある方だけ ^^;

ユーザー JavaScript コードから [fs.chmod()] を呼んだ場合に、この API の実態 (C++ 実装) は [Node.js ソースコード] の何処にあるのか？を追います。

### お題 → fs.chmod()

Node.js 公式の API Documentation に `fs.chmod()` の記載があります。

* Node.js v14.18.2 - [fs.chmod()]

これをお題にコードを追いましょう。

```javascript
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` にあります。

* https://github.com/nodejs/node/blob/v14.18.2/lib/fs.js#L1308

この function の JS 実装はたった６行しか無く、最終行 [L1315](https://github.com/nodejs/node/blob/v14.18.2/lib/fs.js#L1315) の `binding.chmod()` が C++ 側の実装を呼んでいる箇所になります。

```javascript
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](https://github.com/nodejs/node/blob/v14.18.2/lib/fs.js#L59) で生成されています。

```javascript
const binding = internalBinding('fs');
```

### JavaScript → C++ 実装への Binding の仕組み.

Node.js の JavaScript / C++ Binding の仕組み自体は `README.md` に書いてあります。

* https://github.com/nodejs/node/tree/v14.18.2/src#binding-functions

書いてある事は難しいが、要は ↓ という事です。

> *C++ 側で `NODE_MODULE_CONTEXT_AWARE_INTERNAL(モジュール名, Initialize関数)` 定義したモジュールは、JavaScript 側で `internalBinding('モジュール名')` で取得できるよ* 

実際に `lib/fs.js` JavaScript モジュールに該当する C++ 実装は `src/node_file.cc` ファイルになります。

* https://github.com/nodejs/node/blob/v14.18.2/src/node_file.cc

C++ `src/node_file.cc` ファイルの最終行 [L2535](https://github.com/nodejs/node/blob/v14.18.2/src/node_file.cc#L2535) にバインディングの定義があります。

```javascript
NODE_MODULE_CONTEXT_AWARE_INTERNAL(fs, node::fs::Initialize)
```

JavaScript `fs.chmod()` に該当する C++ 実装は、このコード中の以下の関数 [L2114](https://github.com/nodejs/node/blob/v14.18.2/src/node_file.cc#L2114) です。

```javascript
/* fs.chmod(path, mode);
 * Wrapper for chmod(1) / EIO_CHMOD
 */
static void Chmod(const FunctionCallbackInfo<Value>& args) {
  // 省略...
}
```

JavaScript API 名 `chmod` とこの C++ 関数 `Chmod` との紐付けは同ファイルの `Initialize()` 関数内 [L2439](https://github.com/nodejs/node/blob/v14.18.2/src/node_file.cc#L2439) にあります。

```javascript
  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);
```


[JavaScript 技術概説]:https://developer.mozilla.org/ja/docs/Web/JavaScript/JavaScript_technologies_overview
[ECMASCRIPT]:https://www.ecma-international.org/publications-and-standards/standards/ecma-262/
[ブラウザの仕組み: 最新ウェブブラウザの内部構造]:https://www.html5rocks.com/ja/tutorials/internals/howbrowserswork/
[Web IDL]:https://triple-underscore.github.io/WebIDL-ja.html

[DOM]:https://developer.mozilla.org/ja/docs/Web/API/Document_Object_Model
[jQuery]:https://jquery.com/

[Google Chrome]:https://www.google.co.jp/intl/ja/chrome/
[Mozilla FireFox]:https://www.mozilla.org/ja/firefox/
[Safari]:https://www.apple.com/jp/safari/
[Microsoft Edge]:https://www.microsoft.com/ja-jp/edge

[Node.js]:https://nodejs.org/ja/
[Electron]:https://www.electronjs.org/
[Babel]:https://babeljs.io/
[Webpack]:https://webpack.js.org/
[Express]:https://expressjs.com/ja/

[Blink]:https://www.chromium.org/blink/
[Gecko]:https://wiki.mozilla.org/Gecko:Home_Page
[WebKit]:https://webkit.org/

[V8]:https://v8.dev/
[SpiderMonkey]:https://spidermonkey.dev/
[JavaScriptCore]:https://trac.webkit.org/wiki/JavaScriptCore
[Chakra]:https://github.com/chakra-core/ChakraCore

[MDN]:https://developer.mozilla.org/ja/
[JavaScript (ECMASCRIPT)]:https://developer.mozilla.org/ja/docs/Web/JavaScript
[Web API]:https://developer.mozilla.org/ja/docs/Web/API
[setTimeout()]:https://developer.mozilla.org/ja/docs/Web/API/setTimeout
[console.log()]:https://developer.mozilla.org/ja/docs/Web/API/Console/log

[Node.js API]:https://nodejs.org/dist/latest-v14.x/docs/api/
[Node.js ソースコード]:https://github.com/nodejs/node/tree/v14.18.2
[lib/]:https://github.com/nodejs/node/tree/v14.18.2/lib
[fs.chmod()]:https://nodejs.org/dist/latest-v14.x/docs/api/fs.html#fs_fs_chmod_path_mode_callback