こんにちは! バックエンドエンジニアのKazです。普段はLIGの自社サービス開発をおこなっています。
さて、昨年の11月に開催されたNode.jsのイベント「東京Node学園祭2015」に参加してきました。前回の記事に引き続き、Domenic Denicola氏が講演にてさらっと紹介されたes2015(ECMA Script 6、ES6)の新機能を掘り下げてレポートします。
前回の記事はこちらからどうぞ! 東京Node学園祭2015に行ってきた!Domenic Denicola氏の講演レポート vol.1
「東京Node学園祭」とは
Node.jsとは、Webサイトを作る上では欠かせないプログラム言語「JavaScript」をサーバーサイドで動かせるプラットフォームです。そんなNode.jsの普及や情報共有を目指す「Node.js日本ユーザグループ」は、定期的に「東京Node学園」と称したイベントを開催しています。2015年11月には第5回目がおこなわれ、国内外を問わず、数々の著名な方々が登壇しています。
Node.jsについてのより詳しい説明は「ゼロから始めるNode.js入門」を始め、数々の記事がLIGブログのNode.jsカテゴリーにありますので、ご興味のある方はぜひ見てみてください!
それでは、前回の続きからのレポートをお届けします。
The State of JavaScript – JavaScriptのこれまでと、これから(続き)
2. 今日におけるJavaScript(続き)
追加されたデータ構造
es2015ではMapやSetなどいくつかのデータ構造が新しく追加されました。これにより今までオブジェクトや配列では実現が難しかった、あるいは大きな手間とパフォーマンスへの影響があったいくつかの機能が簡単に使えるようになっています。
Map
es2015では新たなハッシュ(連想配列)型として、オブジェクトをキーとして扱える「Map」が追加されました。
JavaScriptではこれまでもオブジェクトがハッシュの役割を担ってきましたが、オブジェクトでは文字列しかキーとして使えなかったため、オブジェクトをキーとして特定するためにはtoString
を実装したり、オブジェクトと値をペアで保有して走査したりするなど工夫が必要でした。
これらの工夫は手間も必要でパフォーマンスにも悪影響があるものばかりでしたが、Mapを使えば簡単にO(1)の計算量で高速にアクセスできます。
Map構造ではこの手間なく直接オブジェクトをキーとして与えることが可能になったので、下記のような書き方ができます。
let map = new Map();
let key = function(){};
map.set(key, [1, 2, 3]);
console.log(map.get(key)); // [1, 2, 3]
for (let key of map.keys()) {
console.log(typeof key); // "function"
}
また、例であげたようにget
、set
といったアクセサーなど、オブジェクトにはなかった痒いところに手が届くメソッドも多数定義されています。
(参照: Cheatsheet / MDN)
Set
また、新しく「Set」も追加されました。これは一言で言うならば「値が重複しない配列」で、同じ値を挿入してもその一意性が確保されます。
let set = new Set([1, 1, 2, "1", 2]);
for (let value of set.values()) {
console.log(typeof value); // 1, 2, "1"
}
Setの特筆すべき点は、まるでハッシュのようにO(1)での値の探査が可能になったこと。通常の配列では特定の値を探すために全ての値を走査する必要がありましたが、SetではO(1)なので高速かつ下記のようにシンプルに処理がおこなえます。
let set = new Set([1, "2", [3], {}, function(){}]);
let item = [4];
set.add(item);
console.log(set.has(item)); // true
set.delete(item);
注意点として、JavaScriptのオブジェクトは値で比較されないため、下記のように別々に初期化されたオブジェクトは異なるものとして扱われます。これはSetの値や前述のMapのキーでも同様に扱われるので注意が必要です。
let arr1 = [1],
arr2 = [1];
console.log(arr1 === arr1); // true
console.log(arr1 === arr2); // false
(参照:MDN)
WeakMapとWeakSet
MapやSetにあわせて、新しく「WeakMap」および「WeakSet」も導入されました。これらはセットされたオブジェクトを「弱い参照」のまま保持するもの。具体的に言うと、WeakMapなどにセットされたオブジェクトが他から参照されなくなったら、ガベージコレクション(GC)によってその値が破棄されるようになるというものです。
この現象を説明する前に、まず下記のコードをご覧ください。
let weakmap = new WeakMap();
function Klass(opts) {
weakmap.set(this, opts);
}
if(true){
let instance = new Klass({attr: 1});
}
// ブロックを抜けた時点でinstanceへの参照がなくなっています。
上記の例では、コンストラクタに渡された値をWeakMapに保存しています。このとき、インスタンスをWeakMapのキーにしていますが、もしこれがMapだった場合は、たとえインスタンスが参照されなくなってもMapからの参照が残り続けるため、結果としてインスタンスが破棄されずに残り続けてしまいます。
しかし、WeakMapを使えば、上記の例で言えばifブロックを抜けた時点でweakmap
にセットされたインスタンスもGCの対象になり、不要なオブジェクトがきちんと削除されるようになります。
これを使うことで、オブジェクト間のアソシエーションなどを表現したり、上記の例の応用でプライベート変数を簡便かつ高いメモリ効率で実装したりするなど、複雑な実装を便利におこなえるようになります。
(参照: Cheatsheet / MDN WeakMap, WeakSet)
反復
es2015では反復(イテレーション)プロトコルが導入され、配列などいくつかのビルトインオブジェクトからIterable(反復可能なオブジェクト)を得られるようになったほか、ユーザー定義のイテレーターも実装可能になりました。
console.log([].entries()); // ArrayIterator
// イテレーターを独自に定義するにはビルトインの「Symbol.iterator」をキーとしてメソッドを定義します
let obj = {};
obj[Symbol.iterator] = function(){
// Iterableオブジェクトはnextメソッドを持たなければなりません
// 返り値はvalueとdoneプロパティーを持つオブジェクトと規定されています
next: function(){
return {
value: "element",
done: false
};
}
};
(参照: MDN)
ゲーム・チェンジャー
また、データ構造や前編で紹介した新しい記法の他にも、数々の便利な機能が追加されています。Domenic氏が“Gamechanger”と表現したこれらの内容は、「試合の流れを変えるもの」という意味がまさにピッタリの革新的な機能ばかりとなっています。
ジェネレーター
es2015からは、新たに「ジェネレーター」が追加されました。これは、非同期処理をまるで同期処理のように扱いやすくしてくれる機能です。後述するPromiseと併せて、複雑なコールバック処理を簡潔な記述に置き換えられるようになります。
非同期処理の実装まで説明を飛ばすと、ジェネレーター関数やyieldキーワードの理解が難しくなるので、まずはシンプルなジェネレーターのサンプルを紹介します。
// function宣言のあとにアスタリスクをつけるとGenerator関数になります。
function* incrementGenerator() {
let i = 0;
while(true){
yield i;
i++;
}
}
// ジェネレーター関数を初期化し、最初にyieldが出現するまで実行して関数の処理を一時停止します。(yieldはまだ実行しない)
let iota = incrementGenerator();
// 以下、next()を呼ぶ都度yieldが実行されます。
console.log(iota.next()); // {value: 0, done: false}
console.log(iota.next()); // {value: 1, done: false}
console.log(iota.next()); // {value: 2, done: false}
console.log(iota.next()); // {value: 3, done: false}
// ...
// この場合yieldが無限ループに入っているので next() は何度でも呼ぶことができます。
// 無限ループしない場合は、yieldがなくなった時点で {done: true} が返されるようになります。
この一時停止の特性を利用して、下記のような同期・非同期処理のコントロールをおこなえます。
let generator = (function* () {
let sleep = function(){
setTimeout(function() {
// 一秒後にメッセージを吐いてnext()を呼ぶ
console.log('Slept.');
generator.next();
}, 1000);
};
console.log('First: '+ new Date());
// ここでsleepを実行した後、タイマーでnext()が呼ばれるまで1秒間停止する。これを待つ間は次のconsole.logは呼ばれません
yield sleep();
console.log('Second: '+ new Date());
// さらにもう一秒停止する
yield sleep();
console.log('Third: '+ new Date());
})();
// コンソールに「First, Slept, Second, Slept, Third」と出力されます。
generator.next();
この例では、yieldを使うことで非同期なsetTimeoutの動作完了を、同期的に待機できることが示せました。この応用として、たとえば連続したAjaxリクエストの返り値をyieldすることで、コールバックのネストを避けたり、複雑な非同期処理を流れを追うように記載できるようになったりします。
ジェネレーターの動作は今までのJavaScriptには存在しなかったものなので、一見戸惑う機能ですが、使いこなすと多彩な処理が可能になります。
(参照: Cheatsheet / MDN)
Promise
いままでJavaScriptではイベントの監視や複雑な処理などをおこなおうとすると、コールバックが複雑にネストしてしまういわゆる「コールバック地獄」に陥ることがありました。
コールバックのネストは冗長な表現となり、記述性や可読性が著しく悪いため、これまでも各所で凄惨なコードを生む原因となっていました。その解決策として、jQuery Defferedなどフレームワークレベルでこれまでさまざまなアプローチが試されてきましたが、es2015ではコールバックをメソッドチェーンで登録・実行できる仕組みがついにPromiseとして標準実装されました。
// ネストしたコールバック
Lorem(function(){
Ipsum(function(){
Dolor(function(){
Sit(function(){
Amet(function(){});
});
});
});
});
// Promiseだとこう書けます。
lorem()
.then(Ipsum)
.then(Dolor)
.then(Sit)
.then(Amet);
Promiseの実装は下記のようにおこないます。
function promise(){
return new Promise(function(resolve, reject){
// 何らかの処理をここに。
// resolve()を呼ぶと処理が続行(次のthenが実行)される。失敗ならreject()を呼ぶ。
resolve();
});
};
promise()
.then(promise)
.then(promise)
.then(promise);
Proxy
(参照: MDN)
es2015では新たに「Proxy」という機能が追加されました。これはオブジェクトを文字通り中継して、代入や検索などの基本的なオブジェクト操作をオーバーライドできるようになる機能です。
let // 基となるオブジェクト
target = {
height: 139.64,
width: "2.45 feet"
},
// Proxyされたオブジェクトに割り当てられるハンドラー
handler = {
// アクセサー (Getter)
get: function(target, name){
if(typeof target[name] === 'number'){
return Math.round(target[name]) + " inches";
} else if(name in target){
return NaN;
}
},
// ミューテーター (Setter)
set: function(target, name, value){
if(typeof value === 'number'){
value *= 2;
}
return target[name] = value;
}
};
let proxy = new Proxy(target, handler);
// 基のオブジェクトを参照
console.log(target.height); // 139.64
console.log(target.width); // "2.45 feet"
// Proxyを介して参照
console.log(proxy.height); // "140 inches"
console.log(proxy.width); // NaN
// 基のオブジェクトを書き換え
target.height = 32.5;
console.log(target.height); // 32.5
console.log(proxy.height); // "33 inches"
// Proxyを介して書き換え
proxy.height = 12.4;
console.log(target.height); // 24.8
console.log(proxy.height); // "25 inches"
(参照: Cheatsheet / MDN)
テンプレート文字列
JavaScriptで文字列の中に変数を埋め込むためには、これまで文字列リテラルと変数、連結演算子を列挙して文字列を結合する必要がありました。しかしこれは複雑になると文字列リテラルやエスケープ記号などが入り組んだりして、記述性や可読性が非常に悪くなる問題も抱えていました。
これを解消するため、es2015では新たに「テンプレートリテラル」が採用され、文字列のテンプレート化が可能になりました。これは文字列をダブルクォート ("
)やシングルクォート ('
) の代わりにバックティック(`
)で囲み、その中に${ ~ }
と記述すると文字列中で変数展開や式の評価が可能になるものです。
具体例は下記をご覧ください。
let url = "http://example.com",
text = "Example";
// 以下2つは同じ。
let link1 = "<a href=\""+ url +"\">"+ text +"</a>",
link2 = `<a href="${url}">${text}</a>`;
console.log(link1 === link2); // true
// 演算も可能。
let expr = `Mean distance is around ${Math.round(321/9)} inches</sup>`;
// 改行もそのまま記述できます。
let ml = `Lorem ipsum dolor sit amet,
consectetur adipisicing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.`;
サブクラス化可能なビルトインオブジェクト
(参照: MDN)
es2015では新たに追加されたclass
宣言と併せて、ビルトインのArrayクラスなどを拡張しサブクラスを定義できるようになっています。
class Elements extends Array {}
(参照: Cheatsheet / MDN)
3. 導入の進むes2015
「es2015 in the wild」と紹介されたこのパートでは、すでに浸透しつつあるes2015の機能や記法を、実際のコードを交えて紹介されました。
新しいコードスタイル
こちらのサンプルでは下記のように新しい記法が採用されています。
- var宣言の撤廃(新しい
let
やconst
の利用) - オブジェクトの短縮記法
- アロー関数の積極的利用
const
宣言による定数の積極的利用- テンプレート文字列の積極的利用
Promiseの活用とジェネレーターによる非同期処理
今記事でも紹介したPromiseやジェネレーターも、その利便性から積極的に利用されています。
Babel
複数のブラウザへ対応するため、新機能や非標準のコードを下位互換のあるコードに変換(トランスパイル)するBabel。これを用いたes2015の利用も幅広くおこなわれています。
さいごに
今回お送りしましたDomenic Denicola氏の講演レポートですが、いろいろとes2015で新規追加された機能を要点的に説明されていたので、この記事では解説を加えてみました。
次回も同じくDomenic氏の講演から、次節「New Platform Innovations」のレポートをお送りいたします。乞うご期待!
LIGはWebサイト制作を支援しています。ご興味のある方は事業ぺージをぜひご覧ください。