2015 年の JavaScript と babel の話
初稿:2015-05-10
はじめに
Node.js 日本ユーザーグループ代表の古川 (@yosuke_furukawa) です。るびま初投稿です。よろしくお願いいたします。
今日は JavaScript の基本的な所に触れつつ、今後の JavaScript である ECMAScript2015 (旧 ECMAScript6 ) の話を中心にしようと思います。
ECMAScript2015 (以下 ES2015 ) は今年の 6 月に公式に次の ECMAScript として仕様が公開されます。この ECMAScript の仕様に準拠した言語実装が JavaScript であり、簡単に言ってしまえば 今後のブラウザ ではこの ES2015 の仕様に準拠した新しい JavaScript が使えるようになります。
ただし、それは今後のブラウザであって、現在普及しているブラウザでは使えません。ES2015 の機能をサポートしたブラウザを待つのではなく、ES2015 で記述し、それを現在普及しているブラウザでも扱えるようにするトランスパイラ babel (旧 6to5) に関して今回は解説します。
また Ruby を記述している方々の中ではウェブのアプリケーションを構築されている方も多いと思いますが、この babel は次の Sprockets の v4.x で導入が検討されており、新しい Sprockets では ES6 で記述することもできる可能性があります。
トランスパイラで何が嬉しいのか
JavaScript におけるトランスパイラというのは、ES2015 以降のセマンティクスで書かれた JavaScript を現在使われているブラウザでも使えるようにするための変換処理をするツールです。
ES2015 は仕様公開を 6 月に控えて、盛り上がりを見せています。
Chrome の開発ビルド (Canary) や FireFox の開発版 (Aurora) にも多くの ES2015 の機能追従がされてますし、一部の機能は既に今のブラウザでも扱えるようになっています。ただし、全ての機能セットを揃えたブラウザは今のところ存在しません。
さらに、JavaScript には他の言語には必ずあるような一般的な機能が欠落している事も多く、それをカバーするために Underscore といったライブラリや CoffeeScript, TypeScript, JSX といった altJS が台頭しています。
ES2015 に対応した JavaScirpt を使うことで、ライブラリの機能が不要になり、依存ライブラリを減らすことができたり、altJS に頼らなくても豊富な言語機能が扱える可能性が広がります。
また数年後には ES2015 が広まっていることを考えるとその時までに ES2015 の新文法、新機能に慣れておいたほうがスムーズな移行が期待できます。
ECMAScript 2015 について
ECMAScript 2015 には、下記の仕様が機能として追加されます。
let/const といった Blocking Scope
Map/Set/WeakMap/WeakSet といった Collections
型を定義する Class
Generator/for..of
Promises
Template String Literals
Arrow Functions
modules
babel について
今一番更新がホットな トランスパイラの一つです。他にもトランスパイラはいくつか存在しますが、新しい文法を一番サポートしているのが babel になります。ちなみに読み方は決まっていません。いろんな読み方を許容しているので、「バベル」でも「ベーベル」でも「バブゥ」でもいいらしいです。
babel サポート文法一覧
http://kangax.github.io/compat-table/es6/
babel インストール
npm を利用します。node.js をインストールしておいてください。
$ npm install babel -g
こうすると 3 つのコマンドラインツールがインストールされます。
babel (babel の基本コマンド、本コマンドを利用すれば ES2015 で記述された JavaScript ファイルをトランスパイルできる)
babel-node (babel でのトランスパイルをした上でコードを node で実行するためのコマンド、REPL にもなる)
babel-external-helpers (babel の utility を babel の外からも使えるようにするためのコマンド)
簡単な使い方としては下記の通り
# babel filename でトランスパイルすると標準出力に結果が出る
$ babel test.js
# babel -o を使うと出力先ファイルを指定できる
$ babel -o test2.js test.js
# babel -w を使うと変更を監視してファイルを出力できる
$ babel -w test.js -o test2.js
# ソースマップを付ける場合は -s, インラインソースマップが欲しいなら -t
$ babel -s -t -w test.js -o test2.js
トランスパイルではなく、スクリプトとしてそのまま実行することもできます。
実行したい場合は babel-node を使います。
$ babel-node test.js
ファイルを省略して実行すると、簡単な REPL にもなります。ただし、改行する度に逐次実行されるため、今のところ一行で全て書く必要があるので REPL としての利用はオススメはしません。
$ babel-node
> [0, 1, 2, 3].map((x)=>x*x);
[ 0, 1, 4, 9 ]
Sprockets で使う方法
sprockets-es6 を使います。
$ gem install sprockets-es6
# Gemfile
gem "sprockets"
gem "sprockets-es6"
require "sprockets/es6"
これで使えるようになります。
gulp/grunt などで使う方法
これもさほど変わりません。grunt-babel, gulp-babel があるのでそれを利用しましょう。
$ npm install grunt-babel load-grunt-tasks -D
require("load-grunt-tasks")(grunt);
grunt.initConfig({
"babel": {
options: {
// ソースマップが要らない場合は false にする
sourceMap: true
},
dist: {
files: {
"dist/app.js": "src/app.js"
}
}
}
});
grunt.registerTask("default", ["babel"]);
これで grunt コマンドを実行すれば babel が実行されます。
また gulp の場合は以下のようにします。
$ npm install gulp-babel gulp-sourcemaps gulp-concat -D
var gulp = require("gulp");
var sourcemaps = require("gulp-sourcemaps");
var babel = require("gulp-babel");
var concat = require("gulp-concat");
gulp.task("default", function () {
return gulp.src("src/**/*.js")
.pipe(sourcemaps.init())
.pipe(concat("all.js"))
.pipe(babel())
.pipe(sourcemaps.write("."))
.pipe(gulp.dest("dist"));
});
これで gulp コマンドを実行すれば babel でトランスパイルされます。
ECMAScript 2015 で変わる JavaScript の書き方
ES2015 にはここでは紹介しきれないほどの機能が入っています。個人的に重要だと思っているものは紹介しますが、全てを見てみたい場合はES2015 のドラフトをご一読下さい 。
ES2015 の目標は先程のドラフトに記述されていて、
ECMAScript 第 6 版のゴールには以下のものが含まれている。
- 大規模アプリ開発の支援
- ライブラリ構築の支援
- 他の言語からのコンパイル対象として利用されること
具体的なエンハンスとして、
- モジュール化
- クラス定義
- ブロックスコープ
- iterator と generator
- 非同期プログラミングのための Promise
- デストラクチャリング
- 末尾呼び出し最適化
といった機能が含まれている。
また、ECMAScript のライブラリとして map や set、binary 配列、Unicode 補助文字、正規表現拡張が built-in で追加されている。
これらの build-in はサブクラスとして拡張するのも可能とする。
という風になっています。つまり、大規模開発に耐えるため、適切にモジュール化をすること、適切な単位でクラスを設計してブロックスコープにより、変数を限定的に扱うことなどができるようになりました。
また非同期プログラミングに関しては Promise として非同期処理を抽象化する事ができるようになりました。さらに iterator や generator を扱う事で遅延繰り返し処理を扱うことができるようになりました。これらの機能は関数型言語の流れを汲んでいると筆者は捉えていて、また末尾呼び出し最適化もこの流れを汲んだ機能追加であると捉えています。
では、これらの機能に対して一つ一つ紹介していきます。また、本当はこの他にも Template String Literals とか Arrow Functions などが定義されていますが、この記事では説明を省きます。
モジュール化
モジュールを切り出すことができるようになりました。これまで JavaScript では言語レベルでモジュールの分割ができませんでした。そのため、JavaScript をモジュール化してフロントで読み込む際には require.js 使ったり、 browserify 使ったりというライブラリで解決するか、global 空間に独自の名前空間を作ってそこに生やすといった処理がされてきました。
ES2015 からはこのモジュール化をするための専用の構文 export と import が使えるようになりました。
基本的には commonjs と似ています、つまり、 export でオブジェクトを import できるようにして、require の代わりに import 構文でオブジェクトを利用できるようにします。
名前付きの export
では実際にモジュールを使ってコードを書いてみましょう。
下記のようなファイルを作成し、Math.js のような名前をつけておきます。
"use strict";
// export 構文で外部から読み込めるようにする
// export する場合は以下のようにする
export const PI = 3.141592;
// import させないものは export をつけないでおく
const _sqrt = function(s, x, last){
return x != last ? _sqrt(s, (x + s / x) / 2.0, x) : x;
};
// 関数に対しても export 可能
// 平方根を求める (バビロニアの平方根アルゴリズム)
export const sqrt = function(s){
return _sqrt(s, s/2.0, 0.0);
};
// 二乗を求める
export function square(x) {
return x * x;
}
これはちょうど commonjs で以下のように記述しているのと同じです。 node.js もしくは browserify を使ってコードを書いたことがある方であれば馴染み深い書き方かと思います。
// Math.js
export.PI = 3.141592;
var _sqrt = function(s, x, last){
return x != last ? _sqrt(s, (x + s / x) / 2.0, x) : x;
};
export.sqrt = function(s){
return _sqrt(s, s/2.0, 0.0);
};
export.square(x) {
return x * x;
};
同様に下記のようなファイルを作成し、 Main.js のような名前をつけておきます。
同じフォルダ内においてください。
import {PI, sqrt, square} from './Math';
console.log(PI); // 3.141592
console.log(sqrt(121)); // 11
console.log(square(11)); // 121
実際に babel-node を使ってコードを実行してみましょう。
$ babel-node Main.js
3.141592
11
121
このように export 構文を使うと import で読み込んで利用できるようになります。
名前付きの import ではなく、export されているものを全て一つのオブジェクトに import する場合は以下のように書きます。
import * as Math from './Math';
console.log(Math.PI); // 3.141592
console.log(Math.sqrt(121)); // 11
console.log(Math.square(11)); // 121
デフォルトの export
前節では、 module について説明しましたが、 export には 2 種類あります。通常の export と default export です。 この違いについて説明しましょう。下記の JavaScript は前回の名前付き export を使っています。
const PI = 3.141592;
const _sqrt = function(s, x, last){
return x != last ? _sqrt(s, (x + s / x) / 2.0, x) : x;
};
const sqrt = function(s){
return _sqrt(s, s/2.0, 0.0);
};
const square = function(x) {
return x * x;
};
export default Math = {
PI: PI,
sqrt: sqrt,
square: square
};
import する側ではこう書きます。
import Math from './Math';
console.log(Math.PI);
console.log(Math.sqrt(121));
console.log(Math.square(11));
先ほどとの違いが分かるでしょうか。default export で export した場合は、import する時に import の対象をブレース {……} で囲む必要はなく、export されている対象の名前を知る必要はありません。
これは、ちょうど commonjs で module.exports を使って書くのと似ています。
後述しますが、export default 構文は class 定義と組み合わせて使うことが多くなると思います。定義したクラスを default export して外から import できるようにする、という書き方が多くなると思われます。
クラス定義
JavaScript でクラスライクなものを作るときは、コンストラクタとして関数を定義し、prototype に対してメソッドを定義することで実現してきました。このような JavaScript をよく見るかと思います。
var Character = function(x, y) {
this.x = x;
this.y = y;
this.health_ = 100;
}
Character.prototype.attack = function(character) {
character.health_ -= 10;
};
これの糖衣構文として class が追加されました。class 構文を使うと以下のように記述することができます。
class Character {
constructor(x, y) {
this.x = x;
this.y = y;
this.health_ = 100;
}
attack(character) {
character.health_ -= 10;
}
}
さっきの書き方よりもスッキリ定義できる上に、クラスであることが直感的に分かるようになりました。以前の書き方は関数定義と同じく function を使った書き方なので、一見しただけでは関数なのか class なのか分かりにくいです。
また先程の module の default export と組み合わせて、下記のように class を公開するやり方が ES6 ベースで記述されたモジュールによく見られます。
export default class Character {
constructor(x, y) {
this.x = x;
this.y = y;
this.health_ = 100;
}
attack(character) {
character.health_ -= 10;
}
}
さて、class があるということは継承も存在します。
継承を使うと以下のように記述できます。
// Character クラス
class Character {
constructor(x, y) {
this.x = x;
this.y = y;
this.health_ = 100;
}
attack(character) {
character.health_ -= 10;
}
}
// 当然継承もある。
// Monster クラスに継承
class Monster extends Character {
constructor(x, y, name) {
super(x, y);
this.name = name;
}
// メソッド書くときはこう書く
attack(character) {
// 親クラスのメソッド呼ぶときはこう
super.attack(character);
// super(character) でも同じ意味になる
}
// get prefix を付けられる
get isAlive() { return this.health_ > 0; }
get health() { return this.health_; }
// set prefix を付けられる
set health(value) {
if (value < 0) throw new Error('Health must be non-negative.');
this.health_ = value;
}
}
var myMonster = new Monster(5,1, 'arrrg');
var yourMonster = new Monster(5,1, 'nyan');
// get prefix をつけるとプロパティアクセスのようにメソッドを扱える
console.log(myMonster.health); // 100
console.log(myMonster.isAlive); // true
// set prefix でも同様。
myMonster.health = 1;
console.log(myMonster.health); // 1
console.log(myMonster.isAlive); // true
myMonster.attack(yourMonster);
console.log(yourMonster.health); //90
これまでの function と prototype を使った書き方よりも直感的な書き方が期待できます。
ブロックスコープ (let/const)
let, const という新しい変数宣言ができるようになりました。これは block スコープと呼ばれています。JavaScript の場合、変数の生存するスコープを表現するのに function で囲む必要がありました。しかし、let/const を使うことで、function だけではなくブレース { …… } で囲まれた領域がスコープになります。
let は再代入可能な変数ですが、const は再代入不可能な変数です。const はちょうど Java で言うところの final があたったような状態になります。
// block.js
{
var a = 10;
let b = 20;
const tmp = a;
a = b;
b = tmp;
// tmp = 30; 再代入はできない SyntaxError になる。
}
// a = 20、a は var で宣言しているのでブロックスコープの外からも参照可能。
console.log(a);
// let で定義した b はブロックスコープの外からは解決できない、ReferenceError b is not defined になる。
console.log(b);
// const もスコープの中でのみ有効、tmp is not defined
console.log(tmp);
iterator と generator
ES2015 から、新しく for of という文法が追加されました。これは繰り返しをおこなう for 文の拡張です。以下の様な記述を行います。
var res = [];
// ここが for of 文
for (let element of [1, 2, 3]) {
res.push(element * element);
}
console.log(res); // [1, 4, 9]
これまでの for 文と何が違うのでしょうか。これまでの for in 文と異なり、of に渡すのはコレクションに限りません。
繰り返し可能なもの、Iterable なものであれば for of 文で繰り返すことができます。
Iterable なものを作るには、 Symbol.Iterator を使います。 Symbol.Iterator の定義は下記の通り。
// 1000 までの値を返す fibonacci を作る
var fibonacci = {
// Symbol.iterator を持つメソッドを持つオブジェクトにする
[Symbol.iterator]() {
let pre = 0, cur = 1;
// iterator オブジェクトは next メソッドを持つオブジェクトを返す
return {
next() {
// next の中では返す値 (value) と次で終わりかどうかを示すプロパティ (done) を返す
[pre, cur] = [cur, pre + cur];
if (pre < 1000) return { done: false, value: pre };
return { done: true };
}
}
}
}
for (var n of fibonacci) {
console.log(n);
}
こうすると、繰り返し可能な任意のオブジェクトを実装することができます。ただし、Symbol.Iterator を使ったやり方は見て頂いて分かる通り、書きやすいものではありません。もう少し簡潔に Iterable なオブジェクトを作るには generator を利用します。
let fibonacci = function*(){
let pre = 0, cur = 1;
while (pre < 1000) {
// ここで destructuring で値を swap させる。
[pre, cur] = [cur, pre + cur];
// yield で値を返す
yield pre;
}
}();
for (let n of fibonacci) {
console.log(n);
}
Promises
成功するか失敗するか分からない非同期の抽象化された状態を持つのが Promise です。
function timeout(ms) {
// Promise の resolve 関数を受け取る
return new Promise((onFulfilled, onRejected) => {
// 50% の確率で onFulfilled, onRejected が呼ばれる
setTimeout(() => Math.random() > 0.5 ? onFulfilled() : onRejected(), ms);
});
}
function log() {
console.log('done');
}
function error() {
console.log('error');
}
// onFulfilled が出たら done、onRejected だったら error と表示する
timeout(100).then(log).catch(error)
デストラクチャリング
デストラクチャリング、和訳すると分配束縛と呼ばれる機能です。Clojure にある機能ですね。
これを利用すると配列やオブジェクトで設定した値を取り出しやすくなります。
具体的には以下のとおり。
var hoge = 123;
var fuga = 456;
// 値を swap する
var [fuga, hoge] = [hoge, fuga];
console.log(hoge); // 456
console.log(fuga); // 123
var [a, [b], [c], d] = ['hello', [', ', 'junk'], ['world']];
console.log(a + b + c); // hello, world (a に "hello", b に ",", c に "world" が入ってる )
var pt = {x: 123, y: 444};
var {x, y} = pt;
console.log(x, y); // 123 444
末尾呼び出し最適化
関数の末尾にある再帰呼び出しを関数で呼ぶのではなく、内部でループに置換することで関数呼び出しのスタック累積をなくし、効率化するという方法です。
module の時に利用したバビロニアの平方根アルゴリズムを元に解説します。ちなみに 2015 年 3 月現在、数多く存在するブラウザ、トランスパイラの中でこの末尾呼び出し最適化を実装しているのは babel だけです。
// バビロニアの平方根
// 関数の最後に再帰呼び出しを利用している事がわかる。
function _sqrt(s, x, last){
'use strict';
if (x === last) return x;
return _sqrt(s, (x + s / x) / 2.0, x);
};
const sqrt = function(s){
return _sqrt(s, s/2.0, 0.0);
};
babel でトランスパイルすると下記のようになります。
function _sqrt(_x, _x2, _x3) {
var _again = true;
_function: while (_again) {
"use strict";
_again = false;
var s = _x,
x = _x2,
last = _x3;
if (x === last) {
return x;
}_x = s;
_x2 = (x + s / x) / 2;
_x3 = x;
_again = true;
continue _function;
}
};
var sqrt = function sqrt(s) {
return _sqrt(s, s / 2, 0);
};
_sqrt 関数の再帰呼び出しが消えて while と ラベル付き continue を使ったループ処理に変換されていることが分かります。
再帰呼び出しは直感的で副作用を少なくすることができる書き方だと言われていますが、関数スタックサイズを消費してしまうため、実行コストがかかります。関数のコールスタックを減らして最適化するのが末尾呼び出し最適化であり、ES2015 の仕様として策定されています。
etc, etc……
この他にも => で関数を定義する Arrow Functions や 変数埋め込みやヒアドキュメントとしても利用可能な Template String Literals、Symbols や Proxy 等、語り尽くせないほど機能があります。
今後の ECMAScript2015 の展望
上に挙げた事からも分かる通り、JavaScript に class や module の考え方が入り、適切な単位でモジュールとクラスを分割して設計することができるようになりました。また let や const で変数の生存範囲を限定する事ができるようになりました。これらの機能は大規模なアプリを開発する時やライブラリを作る際の助けになるはずです。
また、generator/iterator/Promise といった関数型プログラミングの概念が導入され、さらに末尾呼び出し最適化といった副作用を少なくする記述方法ができるようになりました。ES6 には ES5 までの考え方にはないモダンな機能が入っています。
既に ECMAScript の仕様を決めている TC39 は次の ES7 に向けて準備をしています。現時点ではまだまだ検討中ですが、async-await といった非同期呼び出しを同期っぽく呼び出す C# にある機能であったり、Optional Typing の機能をもたらす types や Object の監視をする Object.observe といった機能が検討されています。
これらの機能が全てのブラウザで書けるようになるのはまだまだ先ですが、babel にはいくつか実験的に先行実装されている ES7 の機能 もあります。また babel 単体ではサポートしていなくても flow とあわせることで型チェックを実現したり、jsx とあわせることでかつて存在した E4X のような XML リテラルを記述することができるようになっています。
ここでは紹介しきれませんでしたが、 babel にはこの他にも未定義の変数/関数をチェックする機能 や通らないコードを削除するデッドコード削除の機能 、インライン展開をする機能 などの最適化が入っており、大変高機能になっています。
babel を使って新しい JavaScript を学んでみたい方向けに tower-of-babel という ES6 チュートリアルの学習ツールを作りました。
こちらも使ってみてください。
まとめ
今後の JavaScript である ES6 の話をトランスパイラである babel とともに説明しました。
ES6 の仕様は固まってきてはいるものの、今は仕様のフィードバックを求めている状況であり、この段階で積極的に ES6 を利用していく事で、ES6 の盛り上げを図りたいと思っています。バグや問題があればフィードバック すれば改修される可能性もあります。
また、今回の機能をまとめた tower-of-babel を作ってみました。
是非使ってみてください。
参考文献
著者について
古川陽介 (Yosuke Furukawa / @yosuke_furukawa )。日本 Node.js ユーザーグループ代表、io.js エヴァンジェリスト、io.js コントリビュータ。記事掲載時点では会社でサーバサイドの Perl とフロント JavaScript も行うフルスタックなエンジニア