春からはじめるモダンJavaScript / ES2015

  • 1868
    いいね
  • 4
    コメント
この記事は最終更新日から1年以上が経過しています。

春ですね!人の配置がリファクタリングされ、コードもリファクタリングの季節です。

では僕がここでモダンなJavaScriptとES2015の利点を語る役をやるので、みなさんはチームを説得する役をやってください。

JavaScriptの歴史

まず最初にJavaScriptの歴史を踏まえることで、今学ぶべきものとその理由を確認しましょう。

なぜ2016年の記事でES2016ではなく、ES2015なのか、と疑問に思った方もいるかもしれません。それは、ES2015がただの年次アップデートではなく、これから始まる毎年のメジャーバージョンアップの起点となるバージョンであり、またES5から飛躍的に仕様が増えたバージョンであるからです。

簡単に(雑な)歴史を紹介します。

  • ブレンダン・アイクによってNetScapeに実装/搭載された古の時代〜IE6 (1996~2005)
  • ES3: 一時はシェア7割を誇ったレガシーブラウザ(IE6)でも動く、もっとも知られている実装。jQueryがもっとも普及した時代
  • ES4: 型やクラスの実験的な機能の搭載を試みるが、失敗しpending(これを受け継いだのがActionScript)
  • ES5: 現行のブラウザでほぼ確実に動く水準。ES4の反省を活かし、ES3からは小規模な変更にとどまる。map/reduce などの便利な配列操作がES5に多い
  • ES6(-> ES2015): 仕様策定フローを整理し、class / generator / module などが含まれる最新仕様

ES6はとても野心的で、ある意味ではES4のリベンジだと言えるでしょう。
ただ、策定された時代が違うので、影響を受けたものが異なります。はっきりと言明されたわけではないですが、ES4は主にJavaで、ES6はCoffeeScriptやそれに連なるAltJS群(JavaScriptにコンパイルされる言語のこと)、また node.jsに採用されたcommonjsのモジュールシステムです。(Arrow Function や Class構文にCoffeeScriptの影響を確認できるでしょう )

今年以降はES2016, ES2017... と毎年策定されることになります。どのように決まるかは TC39 についての azu_re さんの記事を読んでください。

TC39 Process

ES2015は、ES5への後方互換性を維持しつつも、大胆な仕様の更新が大量に含まれます。本当にたくさんあって、Chromeの最新版や後方互換性をあんまり気にしていないMS Edge でも、すべての実装が終わっていません。

なぜ2010年代になってJavaScriptの仕様が拡充されたか

これも時代の雰囲気から読みとくしかないのですが

  • Chromeの登場に端を発するブラウザベンダ間の競争(主にMS/Mozilla/Google/Opera)で、HTML5と呼ばれる仕様群が追加されていき、ブラウザのポテンシャルが拡張された
  • 競争によってJavaScriptの速度が向上し、複雑なアプリケーションの構築に耐える速度を持つようになった
  • ↑から生まれたV8(ChromeのJavaScriptエンジン) を使った node.js の普及によって、サーバーサイドJavaScriptで運用していく為に言語のポテンシャルを上げる要求が強くなった

という理解をしています。

ブラウザJavaScript != EcmaScript

ESとはECMAScript の略ですが、ESにはブラウザの仕様は含まれていません。それらは Browser Object Model(BOM, navigator や location など) や Document Object Model(DOM, document や HTMLElement など) と呼ばれ、よく知られているブラウザJavaScriptは、特定の暗黙のグローバル変数(window) 下で動く JavaScript と表現することもできます。

この例として、ブラウザでも node.js でも両方 setTimeout 関数を使うことが出来ますが、それは node.js がブラウザに似せて、60FPS精度の setTimeout 関数を実装しているからですが、node.js ではより細かくスレッドのサイクルに介入できる setImmediate 関数などが追加されました。
(と、書いておいてなんですが、今見る限りchromeにもsetImmediateは逆輸入されていますね…)

このことを覚えておくと、node.js だけではなく、ブラウザ環境でありながら windowではないグローバル変数を持つWebWorker, ServiceWorker, Electron のコードもすんなり理解できると思います。

Universal JavaScript

広義では 「ブラウザとnode両方で動くJavaScript」、狭義では「ESのみで書かれて、この仕様を満たすものならどこでも実行可能」という概念をUniversal JavaScriptと呼んだりします。少し前まで前者のみを指して Isomorphic と呼んだりしていました。

なにか新しいライブラリを書くとき、それが DOM に依存しないものならば、たとえば文字列処理で完結するものならば、DOMに依存しないように書けばそれはUniversal JavaScriptだと言うことができます。

これは 「賢い View(Smart UI) を避ける」というドメイン駆動開発(DDD)の面でも有利であり、モダンなJavaScriptを考えるにあたって、「DOMとロジックの責務が正しく分担されている/分割できる」というのを1つの指標としても考えても良いでしょう。このことから、ほとんどの jQuery Plugin はレガシーだと言ってしまうことができますね。

これが理由で、2016年現在、新たに jQuery Plugin を無節操に追加しまくるのはお行儀が良いことだと思われていません。他の理由としても、ほとんどのコードがgithub以前でバージョンコントロールされていない、というのが理由です。

また品質面でも問題があるものが多く、グローバル変数として内部状態を露出するものまであります。とはいえ、全てのjQueryプラグインを排除するのは、2016年現在においては代替手段がなく難しいと思われます。「用法用量に気をつけて適度に使う」が現実的な選択肢となるでしょう。

ES2015の特長

この記事では、ES2015の意義とそれを採用する利点を語るものであり、ES2015の個別の記事について立ち入って解説しません。ES2015でのJavaScriptの追加機能は多岐にわたり、それらを全て語るのは時間がいくらあっても足りません。とはいえ、Arrow Function と Class Syntax については特記してもよいでしょう。

Arrow Function

Arrow Function は 今まで関数リテラルを function(){...} で書いていたものが、 () => {...} という風にも書けるようになります。また、これで作る関数は、this コンテキストを切り替えません。

関数スコープがthisコンテキストを切り替えるのは今まで数多くの混乱を呼んでおり、いわゆる var self = this; のようなバッドノウハウを使わずに済むようになります。

また単に記法として短いのも魅力であり、JavaScriptの今までの欠点の1つに、setTimeout や map などの組み込み関数などが、高階関数ベースのデザインなのにfunction キーワードが長くて書くのが気持ち的に少し億劫だったのがありましたが、今ではこう書けるわけです。

let names = items.map(i => i.name);
// 引数が1つの場合は括弧が省略できる
// また {...} を使わずに即座に式を入れることで、その評価した値を返す

Class Syntax

Class Syntax は、オブジェクト指向デザインでよくある、よくあるクラス構文です。
とはいえ、元のプロトタイプベースのデザインを捨て去ったわけではありません。内部的にはプロトタイプベースの上でクラスの振る舞いと等価となるようなコードに変換されます。

class X {
  foo() {
    return "foo";
  }
}
let x = new X();
x.foo();
X.prototype.foo.call(x); // 昔ながらのprototypeが定義されている

普通ですね。

ES2015を知れば知るほど、総じて、「普通の言語になった」という印象を受ける人が多いと思います。

変えるには遅すぎた仕様

ES2015は多くの革新的な飛躍を含みますが、基本的にはES5との後方互換性を維持しているので、言語の根幹に関わる機能は、たとえ批判が多くともそのままになっています。いくつか例をあげます。

  1. for文 や if文 がレキシカルスコープを作らない
  2. == の曖昧な比較
  3. トップレベルの暗黙のグローバルスコープ

1については for(let i = 0; i < 10; i++){...}let を使うことで初期化子についてはレキシカルスコープを導入できます。または forEach や map の高階関数で関数スコープを作ります。

2についても、「== を使うな、===を使え」、で済ますことができます。== は比較前に暗黙のキャストが行われ、 1 == "1" //=> true などの不可解な挙動があります。

3はちょっとややこしいかもしれません。ブラウザ環境で先頭に var x = 3; などとすると、 window.x = 3; となります。これは window がブラウザ環境の暗黙の this であり、this のスコープを作る function(){...} で囲まれない限りトップレベルの変数宣言は window へのメンバの追加と同じになります。

また、スコープ内に変数x がないときに x = 3 とすると、それは暗黙にトップレベルスコープの変数定義 = グローバル変数の定義となってしまいます。これに関しては "use strict"; を使うことで、警告を出すことができます。

(注: 自分の理解はこうですが、仕様的に正しい表現ではない可能性があります)

ちなみに、node 環境下ではファイルスコープの概念があるので、他のファイルにトップレベルの変数宣言が共有されることはありませんが、同じ package.json に属する単位で共有される global という暗黙のグローバル変数があります。

これらを変更しつつ後方互換性を維持することは困難で、今後も修正されることはないでしょう。(use xxx; のようなマジックコメントで挙動を切り替えられるようになる可能性はあります)

いますぐES2015を使う

トランスパイラで今すぐES2015を使う

ここまで述べたES2015ですが、その仕様の多さのあまり、ほとんどの機能が現行のサポートすべきブラウザで実装されていません。

で、ES2015で書いたコードをどう扱うかというと、トランスパイラと呼ばれるものでES5(場合によってはES3)に変換します。メンテすべきコードをES2015以降に保ちつつ、実行コードはES5という状態なら、殆どのブラウザにおいてランタイムでの動作が保証されます。

メジャーなES2015トランスパイラに以下の2つがあります。

  • babel: プラガブルにシンタックスを入れ替え可能なトランスパイラ。作者はsebmck(現facebook)
  • typescript: 型アノテーションのシンタックスの拡張とその静的解析を行なう。MS製。

型に注力した typescript が ES2015のトランスパイラと言われると違和感があるかもしれませんが、enum, module 等の一部の機能を除けば、現在ではほぼ es2015互換と言ってよいでしょう。tsc のコンパイルオプション --target ES6 は、ほぼ型宣言を除去するだけです。

babel にも flowtype と呼ばれる静的解析ツールがあるのですが、閉じた環境で使う限りはいいものの、外部の型定義ファイルの多さなどのエコシステムは typescript に劣ります。

モジュールシステムの現状

ES2015 にはES Modules と呼ばれるモジュール機構があるのですが、これらは現状どのブラウザでも動きません。

ECMAScript 6 modules: the final syntax

その理由は、そのファイルが何を依存として持つか、そのソースをパースするまで不明、という事情があります。パフォーマンスを度外視するなら、一つずつファイルを落としてクライアントで評価し、その結果またリクエストを発行し…と繰り返せばいつか終わりますが、大量に分割されたファイルでそれをやるのは現実的ではありません。

現実には「アセットの配信サーバーがJSを静的に解析して依存を連結できる or メタファイルでそれを通知して連結する」 & 「ブラウザが複数のJSが連結されたbodyを分割して評価できる or HTTP/2 環境で1本のストリームで受け取って順繰りに評価できる」という機構が必要です。要はどのアプローチも難しいです。

また、node.js でもES Modules をどう扱うかは決定していません。

それらの妥協策として、事前に開発環境でJSを静的解析し、くっつけてしまったものを配布するのが主流です。

Browserify/Webpack

ES Modules の import/export 宣言は、そのファイルスコープのトップレベルで、必ず静的に(変数を使わずに)行う必要があります。これによって、事前にそのファイルの依存グラフが確定するわけですが、これは開発環境下でやってしまうことができるので、それらを行なう専用のツールがあって、BroweserifyとWebpack が使われることが多いでしょう。

これらのツールを使ったアプローチは、「ES Modules を CommonJS に変換 => CommonJS をbrowserifyで連結」となります。

このように、モダンなJSエンジニアは、コードを変形させることに抵抗が少ないです。これは、ウェブは仕様が進むのが遅い世界だったので、AST(抽象構文木)の方が先に安定し、その変形によって後方互換性を守りつつ、開発現場では未来の仕様を先取りする、というのがAltJSが流行って以降は一般的になりました。

node.js ツールチェイン

babel or typescript / broweserify or webpack ときて、次にくるツールは npm と gulp です。
最近のモダンなJavaScriptでは、ライブラリの依存を npm 経由で解決するのが主流です。昔のように、ライブラリを1個1個リポジトリにコミットする必要はありません。

browserify(commonjsのリゾルバ) では、相対パス以外で書かれた依存パスは npm から解決します。

var React = require("react");
var myModule = require("./mods/my-module");

というようなコードになるわけですね。

gulp はファイル監視して任意のタスクを実行するツールです。ググってください。(昔は grunt ってのもありましたが、最近ほとんどきかなくなりました。メンテはされているようですが)

gulpjs/gulp: The streaming build system

とはいえ、ほとんどのユースケースでは browserifyをオンメモリに変換の中間状態を温めて高速化し、ファイル監視機能を足した watchify で完結するのではないでしょうか。

Watchifyでbrowserifyを差分ビルド - Qiita

若干、駆け足になりましたが、ここで言いたかったことは、2016年の現時点でモダンなES2015やモジュールを使いこなしたければ、node.js のツールチェイン群にある程度習熟する必要があります。

「モダンな」JavaScriptへの批判

自分もこの点は問題だと思っているのですが、コンパイル(ES2015=>ES5)、ビルド(連結)の過程がある以上、ツールチェインがどうしても複雑になります。またビルド時間も(5秒を越えることは稀だと思うのですが)長くなります。

また中間生産物が多くなり、何を.gitignoreに入れるか考えたり、CI上でどう扱うか、デプロイ時にどこからどこまで行なうか、それらを考え始めると、結構辛いものがあります。

これらの問題はいろんな解決方法がQiitaかQiitaでないか問わずシェアされていると思うので、自分にあったものを見つけてください。

ちなみに、僕自身の判断基準として、「watchifyで完結する以上のことはやらない」という信条があります。gulpの使用すらできれば避けたい。npm に最初から組み込まれている、npm scripts を使う方法もあります。

参考: npm で依存もタスクも一元化する - Qiita

mizchiのおすすめスタック

ちなみにこのサイト(Qiita) も、coffeeとsprocketsで書かれていたものを、僕が入ってから上のスタックに書き換えました。rails上なのでwatchifyではなくbroweserify-rails を使っていますが。

コードの行数が3000行を越える見込みがあったり、そのアプリケーションによる型の利点を最初から認識できるのならば、babelの代わりにtypescript を使うのがいいと思います。Qiitaはシングルページアプリケーションではない、いわゆる「普通のウェブアプリケーション」なので、自分以外のエンジニアの学習コストの問題と、Railsアプリケーションの定義から型情報を取り出せず、型の恩恵にあずかることが困難だとして、typescriptを諦めました。型好きなんですけどね。

追記: ↑の最小環境のセットアップ

# npm のインストールは省略
npm init # package.json
npm install -g watchify
npm install -D babelify babel-preset-es2015
echo "{\"presets\": [\"es2015\"]}" > .babelrc
watchify -t babelify -o dist/bundle.js main.js # パスは環境に応じて

まとめ

ES2015でJavaScriptは「まともな言語」になりました。そしてまだまだ進化する余地があり、これからも進化し続けるでしょう。WebAssembly や Type Annotation の仕様化については今回取り上げなかったのですが、以前と違って毎年仕様は更新され続けるので、TC39をウォッチしましょう。それから、 caniuse.com をみるのも忘れずに。

Can I use... Support tables for HTML5, CSS3, etc

ですが、現状ではモダンなエコシステムの恩恵にあずかろうとすると、多少の困難を伴います。ただしくトレードオフを見極めて、あなたのアプリケーションに最適な開発スタックを作ってください。

ここから先は、あなた自身の戦場での戦いです。