Closed27

「Node.js 15」を「PHP 8,Python 3.9」と同列に見ることができない理由

https://min.togetter.com/Hqz7ufB

この記事を読んで、Node.jsが他の言語の何に相当するかを整理したくなった。

雑に流れで書いたのでめちゃくちゃ余計な方向に話が進んでいる。そのうちタイトルを変える予定。

何らかのプログラミング言語で書かれたプログラム、ソースコードは、CPUが実行できる機械語に変換して実行しなければならない。変換作業を行うのが言語処理系で、大まかに コンパイラインタプリタ という分類がある。コンパイラはソースコードを実行前にまとめて機械語に変換 (コンパイル) し、インタプリタはソースコードを逐次解釈して機械語に変換しながら実行する。高校・高専・専門・大学関係なく情報を学んだことがあるなら習ったことがあるだろう。

例えば Borland C++ Compiler はC/C++のソースコードをコンパイルして実行ファイルを出力する。ここで押さえておいてほしいところは 言語の仕様と言語処理系は別物 だということだ。

C言語には ANSI C, C99, C11 といったようなバージョンがある。これは言語仕様のバージョンである。たとえばANSI Cではブロックの先頭でしか変数宣言できなかったが、C99ではどこでも宣言できる。このような仕様は主にISOとかが文章の形にまとめており、処理系を作る人は仕様書を読みながら仕様に準拠した処理系をがんばって作るのだ。

C11の仕様書とにらめっこしながら作られたC言語処理系は「C11準拠の処理系」と呼ばれる。

「コンパイラ型言語」「インタプリタ型言語」という違いも、デファクトスタンダード (事実上の標準) になっている処理系のタイプによる分類であって、本質的な言語の分類ではないことに留意する必要がある。

そもそもインタプリタっぽい挙動でも中で JIT コンパイルとかいってコンパイルっぽいことしてたり、Javaバイトコードや.NETのCILに変換してから実行時に機械語に変換してたり、コンパイラとインタプリタのいいとこ取りを狙うのが流行している。

C言語のわかりやすいところは、「C」とかいう名前の処理系がないところだ (Clang は微妙な線だが…)。一方で言語 (仕様) の名前と (一般的な) 処理系の名前が同じになっている言語もある。PHP は言語の名前が PHP なのに対し、標準インタプリタのコマンド名も php 。同様にPythonのインタプリタも python だし、Ruby のインタプリタも ruby だ。

では JavaScript はどうか。JavaScript は C と同様、仕様書と処理系が別である。規格書は ECMA International という団体が書いてるので ECMAScript と呼ばれている。ECMAScriptにもバージョンがあり、バージョン3, 5 と来て、その次の 2015 以降は毎年バージョンが更新されている。処理系を作る人は ECMAScript 2018 の仕様書をディスプレイに穴が空くほど見つめながら処理系を書くのだ。

JavaScript は御存知の通りブラウザで動作する言語なので、ブラウザが言語処理系を搭載している。Chromium が搭載しているのが V8 で、Mozilla Firefox が搭載しているのが SpiderMonkey である。Chromium 派生の Google Chrome, Microsoft Edge,Vivaldi, Sleipnir, Blave, Opera 等は V8。

Internet ExplorerはChakra、旧EdgeはChakraCoreを使っていた。

JavaScriptはブラウザで動作する言語だし、元来ブラウザで動作させるための言語だったが、こいつをサーバ上で動かそうという人間 (Ryan Dahl; ライアン・ダール) が現れる。こうして Node.js が誕生した。こいつは Chromium と同じ V8 を使う JavaScript 実行環境である。Node.js のコマンドは node である。js ではない。

ライアンは最近になって Node.js のだめな所を改善した Deno を開発した。こいつも V8 を使う JavaScript 実行環境である。

処理系と実行環境の違いに自信がないが、たとえばブラウザなら Web 関連の、Node/Deno なら OS 関連の API が追加されているとかそういうあたりが違いだろうか…。誰か詳しい人教えて下さい。

このように、JavaScript は ECMAScript という規格と V8, SpiderMonkey といった処理系 (の名前) が分離されている。

たとえば「Node.js 14 で V8 が 8.1 に上がって ECMAScript 2020 の オプショナルチェイン演算子と Null 合体演算子が使えるようになったんですよ~」といった会話が可能。

Python の標準処理系は CPython と呼ばれることもある。C言語実装であることからの呼び名で、Pythonによるセルフホスト実装の PyPy, .NET で動く IronPython, Java仮想マシンで動く Jython といったその他の処理系の登場で区別する必要が生じて生まれた呼び名でもある。

「PyPy は CPython よりxx倍早いんですよね」みたいな会話があったりするかも。

Pythonはバージョン2と3で非互換なせいで python2 python3 という2つのCPython処理系がシステム上で共存してることが普通によくある。

IronPython などの開発者は (たぶんだが) Python 言語リファレンスを熟読して処理系を書いているはずだ。このリファレンスのバージョンは3.8.6のようにCPythonに合わせて作られている。Pythonの規格はCPythonが大本になっている部分がありそうだ。Rubyもデファクトスタンダードの MRI (Matz's Ruby Interpreter) 実装が実質的な仕様書のようだ。

(私はPHP, Python, Ruby には全然詳しくない)

C は「The Programming Language C」をもとに非互換な処理系が乱立したので共通の規格が作られた。 JavaScript も Netscape のオリジナル をもとに各ブラウザ (IEとか) が好き勝手拡張して互換性がなかったので ECMAScript という規格にまとめられた。仕様と処理系の分離はこのような経緯をたどった言語が持っていることが多い気がする。

Rust のコンパイラは rustc という名前である。Rust 1.48 で〇〇が変わった、というのは rustc を指している場合が多い。rustc --version を実行するとバージョンがわかるだろう。筆者の環境は1.45であった。最近 Rust 放置していることがバレて恥ずかしいので今アップグレードして1.48にした。

Java は Java 言語のソースを javac でJavaバイトコードに変換してJava仮想マシン java で実行するステップを踏む。さらに Java には Oracle JDK と Oracle OpenJDK, AdoptOpenJDK といった実装のバリエーションがあり、そして当然ながら JavaScript とは違う。Java について語る際はこれらの単語の違いをまあまあ把握する必要があると思う。

話を戻すが Node.js はサーバサイドアプリを動かすだけでなく、JavaScriptで書かれたツールなどを動かす環境としても使われてきた。フォーマッタ(Prettier), リンタ(ESLint), テスト(Jest)といったものである。Babelは、古いブラウザで使えないECMAScript2020などのコードを、ES5準拠などの古いコードに変換したり、規格にない拡張記法のJSXを変換したりするJS→JSトランスパイラ。webpackは、npmから引っ張ってきたライブラリとソースコードを結合 (バンドル) してブラウザでも実行できるようにするモジュールバンドラ。そして TypeScript と Flow に至っては新たな言語の処理系である。

これらツール類はNode.jsで動く。サーバとしてNode.jsを使わない場合でも、Node.jsを開発時に使う場合があるということ。

これはPythonにおけるCPythonやPHPにおける標準インタプリタと同じ役割である。しかし、PythonではCPython、PHPでは標準インタプリタのバージョンが実質的に言語仕様のバージョンになっているのに対して、Node.jsはあくまでECMAScriptという規格に沿って開発される処理系であることが異なっている。

そのため、Node.js 15 と Python3.9、PHP8はまったく同じものとしては語れない。

特に 「IronPythonがPython3.9準拠バージョンをリリース」ということは言えるが、「SafariがNode.js 15 準拠バージョンをリリース」とは言えない。Node.js は言語の仕様ではないからだ。言語仕様である ECMAScript を使って「ECMAScript 2020 準拠バージョンをリリース」と書く。

GatsbyはWebサイトを構築するフレームワーク。Node.jsで動き、内部では webpack と Babel を使っている。ページはReactコンポーネントとして書き、それを gatsby build で静的なHTML+CSS+JSに変換し、それをデプロイする。

create-react-app や普通の React ではページの内容はブラウザでJSが実行される段階で表示される (CSR; Client Side Rendering)。
JavaScript が使えない環境 (curl など) では <script> タグしか見えないと思う。
検索エンジンのクローラがJavaScriptを解釈できないのでSEO的に不利、という意見も見られた (2020年現在では解釈実行できるっぽい?)。

GatsbyがReactコンポーネントを素のHTMLに変換する作業も、Node.jsで行われている。そのとき Reactコンポーネントが描画される、すなわち関数が Node.js で実行される。Node.js で実行されるということは、コンポーネント内で Node.js で使えない機能、たとえば localStorage とかにアクセスしている場合、Uncaught ReferenceError: localStorage is not defined で死ぬ。このような、ブラウザでしか実行させたくない (実行できない) コードは React.useEffect のコールバック関数内に書く必要がある。

Gatsby は gatsby-node.js というファイルでビルド時の特定のタイミングで関数を実行できる (いわゆるフック)。その名の通りビルド時、すなわちNode.jsでのみ実行されるので、process のような Node.js 特有の機能を使ったコードを書いてもよい。

Next.js, Nuxt.js はよく知らないが、Static モードでサイトを作るなら Gatsby と同じになると思う。勉強してやる気があったら書く。

個人的にはJavaScript入門をNode.jsから始めるべき、というのは微妙な話である。「(プログラミング経験者が) JavaScript 入門」なのか、「JavaScript (でプログラミング) 入門」なのかで別の話題になると思う。
私は JavaScript から入門したといっていい程度に JavaScript 重点で暮らしてきたので、他言語から JS に入門する人の気持ちを知るのは割と新鮮であった。

TypeScript は処理系である tsc によってJavaScript に変換される。高級言語→高級言語の変換をトランスパイルと呼ぶことからTypeScriptをトランスパイル型言語と呼ぶこともできる。ただしtscがtscなんて名前をしているので単にコンパイルと呼ばれることが多い。トランスパイルって長いし。

ブラウザで実行できる言語がJavaScriptしかないおかげで、JavaScriptへのトランスパイラはわりと多い。Opalとか。

このスクラップは2021/02/04にクローズされました
ログインするとコメントできます