見えてきた「ECMAScript 6」。JavaScriptの生みの親が書く「Harmony of Dreams Come True」
JavaScriptの標準仕様となっているのがECMAScriptで、最新バージョンは2009年12月に策定されたECMAScript 5th Editon。そして次のバージョンとなるECMAScript 6は、コード名「Harmony」もしくは「ES.next」や「ES6」と呼ばれています。
ECMAScript 6にはどのような機能が加わるのか、JavaScriptの生みの親であるBrendan Eich氏が、自身のブログに「Harmony of Dreams Come True」というエントリをポストし、その内容を紹介しています。PublickeyではEich氏の許可を得て日本語訳を掲載します。
(正確な翻訳に務めましたが、言語仕様やガベージコレクションなど難解な部分が多く、もしも間違いなど発見されましたらコメントやツイートなどでご指摘ください。また最後の細かい仕様については訳を省略しました)
Harmony of Dreams Come True
このブログは、「Strange Loop 2012」のクロージングキーノートでプレゼンテーションをして評判の良かった、新しいES6(EcmaScript 6)の一部分にフォーカスしたものになっている(reveal.jsベースのHTMLスライドになっており、いくつかは「Fluent 2012」の私のキーノートからで、多くはDave Hermanが「Web Rebels 2012」で話したものだ(感謝!)。ここでスライドが見られて、Jason Rudolphのメモも見られる)。
2011年の初頭にHarmony of My Dreamsというエントリをブログに書き、1ページに収まる程度にECMAScript Harmonyにおいて重要な要素となるだろうと私が夢のように描いていたものを記した。この名前はJavaScriptの将来の標準のために私が作ったものだ。
いま、この夢が現実になろうとしている。ES6のドラフト仕様だけでなく、主なブラウザのプロトタイプ実装として。私は約6週間前にリリースされたFirefox 15(そう、つまりこれはFirefox 16が翌日には登場し、Firefox 17 betaと18 auroraも -- これらすべてがごきげんな新機能を備えていることを意味する -- 高速リリースは楽しいよね?)の利用をお薦めする。というのも、このMDNドキュメントによると、Firefox 15で採用されたSpiderMonkey JSエンジンではこれから紹介するようなES6の機能が新しくプロトタイプとして実装されているためだ。
Default parameters
この拡張(別名“パラメータデフォルト値”)は非常に親切だ。引数(arguments)を空いた部分にも置いてくれるのだ。
js> function f(a = 0, b = a*a, c = b*a) { return [a, b, c]; }
js> f()
[0, 0, 0]
js> f(2)
[2, 4, 8]
js> f(2, 3)
[2, 3, 6]
js> f(2, 3, 4)
[2, 3, 4]
実装者としてのクレジットはBenjamin Petersonで、彼のこの実装と、Jason Orendorffのいつも素晴らしいコードレビューが行われた。最新のES6合意仕様に追随するには、このバグを参照し、undefinedの扱いがどうデフォルト値となるべきかを参照すること。
Rest parameters
これはデフォルトパラメータよりもさらに親切であり、さらに引数(arguments)について細かいことを覚えなくてもよくしてくれる。
js> function f(a, b, ...r) { print(Array.isArray(r)); return r.concat(a, b); }
js> f(1, 2)
true
[1, 2]
js> f(1, 2, 3)
true
[3, 1, 2]
js> f(1, 2, 3, 4, 5)
true
[3, 4, 5, 1, 2]
これもBenjaminとJasonの仕事だ。
Spread in array literals
Rest parameterのrestと対をなすものが“spread”と呼ばれ、コール式(call expression)や配列(array)のリテラル中でも有効となる。以下はForefox 16での実装。
js> a = [3, 4, 5]
[3, 4, 5]
js> b = [1, 2, ...a]
[1, 2, 3, 4, 5]
再びBenjaminとJasonに感謝。
Spreadのコール式(call expressions)での実装はまだだ。
js> function f(...r) { return r; }
js> function g(a) { return f(...a); }
typein:20:0 SyntaxError: syntax error:
typein:20:0 function g(a) { return f(...a); }
typein:20:0 .........................^
しかしもうすぐだろうと思う -- bug 762363に注目だ。
for-of iteration
これについては、以前ブログに書き、TXJS 2011でも話した。この文脈上の(contextual)キーワード“of”は、CoffeeScriptにもあり、新しいiteration protocol(これはPythonのものが基となった)が起因となって、for-inループのinの場所に置かれることになった。
js> for (var v of [1, 2, 3]) print(v)
1
2
3
ES6では配列はイテレート(繰り返し)可能である。これは大きなユーザビリティをもたらす! 不注意な初学者がPythonぽいイテレートができると思って、値ではなくキー文字列が取り出されてしまうfor-inの罠にはまるのを避けることができる。
オブジェクトはプログラマーによる明示的な指定なしにはイテラブルにならない。
js> for (var [k, v] of {p: 3, q: 4, r: 5}) print(k, v)
typein:24:0 TypeError: ({p:3, q:4, r:5}) is not iterable
指定するには、iterator factoryをコールする。これはパラメータ用に新しいイテレータを返す関数だ。あるいはシンプルに、オブジェクトか、値がiterator factory method:a関数、つまり求められる新しいイテレータをthisパラメータに渡すような共通プロトタイプのiteratorプロパティを渡す。
明示的な指定を求めることで、コレクションオブジェクト(collection objects)のカスタムイテレータに対する将来的な障害にならないようにした。こうしたオブジェクトはおそらく、あらゆる種類のジェネラルプロパティ・イテレータ・デフォルト(general property iterator default)を要求することはないだろう
最も簡単なiterator factoryを作る方法は、ジェネレータファンクション(generator function)を書くことだ。
js> function items(o) { for (var k in o) yield [k, o[k]]; }
js> for (var [k, v] of items({p: 3, q: 4, r: 5})) print(k, v)
p 3
q 4
r 5
この例もdestructuringを用いている
SpiderMonkeyはまだES6のジェネレータfunction*シンタックスを実装していないことを記しておく。またES6のyield*を用いたsub-generatorへのデレゲーティング機能およびジェネレータからの値のリターンも実装していない。まもなくだろう。
Map
これまで任意のキーを値にマップしたい、ただしキーを文字列には変換せず衝突もさせない状態で、と思ったことはないだろうか? ES6のMapはそれができる。
js> var objkey1 = {toString: function(){return "objkey1"}}
js> var objkey2 = {toString: function(){return "objkey2"}}
js> var map = Map([[objkey1, 42], [objkey2, true]])
js> map.get(objkey1)
42
js> map.get(objkey2)
true
このMapコンストラクタは、arrayだけでなくどんなイテラブルなものでも用いることができ、キーと値のペアに対してイテレートする。
もちろんMapに登録された値のアップデートもできる。
js> map.set(objkey1, 43)
js> map.get(objkey1)
43
そして任意のキーと値の型を追加できる。
js> map.set("stringkey", "44!")
js> for (var [k, v] of map) print(k, v)
objkey1 43
objkey2 true
stringkey 44!
js> map.size()
3
キーを値として用いることさえできる。
js> map.set(objkey2, objkey1)
js> map.set(objkey1, objkey2)
js> for (var [k, v] of map) print(k, v)
objkey1 objkey2
objkey2 objkey1
hi 44
stringkey 44!
しかし現在、objkey1とobjekey2のエントリのあいだにはサイクルが存在する。これはテーブルに結びついた領域であるため、サイクルを破壊して手動でリリースしなければならない(あるいはマップのすべての参照をドロップする)。
js> map.delete(objkey1)
true
js> map.delete(objkey1)
true
js> for (var [k, v] of map) print(k, v)
hi 44
stringkey 44!
objkey1とobjkey2の値をnullにセットするには、サイクルによって結びついたmapのなかの領域を開放する(free)だけでは十分ではない。map.deleteが必要だ。
もしもあなたのmapが、任意のキーとバリューを渡されてmap.setで値がセットされるAPI で利用されないのなら、サイクルについて心配する必要はないし、あるいはもしもmap自身がもうすぐガベージとなれば心配不要だ。しかし任意のキー/バリューに対してリークプルーフとするには、後述のWeakMapを参照のこと。
Set
任意の値のセットがほしいとき、mapと冗長なコードを書くのは面倒だ。ということでES6にはsetがある。
js> var set = Set([1, true, "three"])
js> set.has(1)
true
js> set.has(2)
false
js> for (var e of set) print(e)
1
true
three
js> set.size()
3
Mapと同じように、Setではaddと同様にdeleteもできる。
js> set.delete("three")
true
js> for (var e of set) print(e)
1
true
js> set.size()
2
js> set.add("three")
js> set.size()
3
オブジェクト要素のキー化による特定は、ほかの型の要素と同じように利用できる。
js> var four = {toString: function(){return '4!'}}
js> set.add(four)
js> set.has(four)
true
js> for (var e of set) print(e)
1
true
three
4!
Mapとは異なり、任意の要素によるサイクル的リークの危険はない。WeakSetがオブジェクト要素を用い、残っているオブジェクトに対して参照するものがなくなった時点で自動的に要素の削除をしてくれのは便利ではあるが。これはproxiesやsymbolsにも関連してくるが、それはまた別に書くことにしよう。
WeakMap
前述したとおり、Mapではキーとバリューのmapのあいだにはサイクルを作ることになり、テーブルの領域と結びついてしまう。そしてヒープ中のすべてのオブジェクトは、サイクルあるいはそのオブジェクトから参照可能なものとつながってしまう。たとえテーブルの外からキーオブジェクトを参照するものがなくなったとしてもだ。リテラルな文字列によって再作成されたノンオブジェクトなキーにはそうした危険はない。
ES6のWeakMapはそうした課題を解決する。
js> var wm = WeakMap()
js> wm.set(objkey1, objkey2)
js> wm.set(objkey2, objkey1)
js> wm.has(objkey1)
true
js> wm.get(objkey1)
({toString:(function (){return "objkey2"})})
js> wm.has(objkey2)
true
js> wm.get(objkey2)
({toString:(function () {return 'objkey1'})})
ここまではいいだろう。wmはサイクルを持っているが、objkey1とobjkey2の値は依然としてオブジェクトを保持している。そこで外部からの参照を切り、ガベージコレクションを起こしてみよう。
js> objkey1 = null
null
js> gc()
"before 286720, after 282720\n"
js> wm.get(objkey2)
({toString:(function () {return 'objkey1'})})
js> objkey2 = null
null
js> gc()
"before 286720, after 282624\n"
この時点でwmは空(empty)だ。WeakMapを数えることはできないし、ガベージコレクションのスケジュールも分からないけれども(ブラウザーではgc()で強制することもできない)。それにwm.hasで証明することも、objkeyへの参照がnullだからできない。
つまりWeakMapはJavaScriptのガベージコレクターと密接な関係にある。ガベージコレクターはいつまで参照のないオブジェクトが残るのか、などを知っている。
特別なガベージコレクションの扱いはオーバーヘッドになる、それは通常、Mapユーザーを煩わせるべきではない。
さらに、WeakMapはキーを忘れることはない(no-foget-key)というルールが適用され、オブジェクトキーのみを受け入れる。これはガベージコレクタが不要になったキーのエントリを集めるために必要である。
例えば42、あるいは“42!”というキーのエントリは、キーが持つ基本的な値(primitive value)のコピーが存在しなくなればガベージコレクションの対象となるだろう。たとえ値がいつ再作成されようとも(primitive型はvalueアイデンティティは持つがリファレンスアイデンティティは持たない)。
もちろん、ガベージコレクションは42のライブインスタンスを効率的に数え続けることはできないか、まったくできない。それはJavaScriptエンジンの実装の詳細に依存することだ。そして文字列は参照経由では共有されず、数えられる。
こうしたことは難しい話で、おそらく平均以上のMap利用者なら知っておくべきだが、weak reference(ES7の視野に入り始めた!)とmapファシリティーズの分離と比べると、WeakMapに必要だというのが本当のところだろう。
Smalltalkのひとたちは10年以上前にこのことを発見し、そしてweak key/valueペアをEphemeronと呼んだ(@awbjsはこの発見の証人であり、Wikipediaのクレジットは不十分だと証言してくれた)
Proxy
ES6のドラフト仕様はProxiesの最初のプロトタイプから前進している。嬉しいことに、Tom Van Cutsemのharmony-reflect libraryによって、新しいProxy仕様は古い仕様(SpiderMonkeyとV8でプロトタイプが動いた)の上に実装可能だ。さらに嬉しいことに、SpiderMonkeyではdirect proxiesの実装が組み込まれた。
Tomの__noSuchMethod__実装はdirect proxiesを用いている。
js> var MethodSink = Proxy({}, {
has: function(target, name) { return true; },
get: function(target, name, receiver) {
if (name in Object.prototype) {
return Object.prototype[name];
}
return function(...args) {
return receiver.__noSuchMethod__(name, args);
}
}
});
js> void Object.defineProperty(Object.prototype,
'__noSuchMethod__',
{configurable: true, writable: true, value: function(name, args) {
throw new TypeError(name + " is not a function");
}});
js> var obj = { foo: 1 };
js> obj.__proto__ = MethodSink;
({})
js> obj.__noSuchMethod__ = function(name, args) { return name; };
(function (name, args) { return name; })
js> obj.foo
1
js> obj.bar()
"bar"
js> obj.toString
function toString() {
[native code]
}
このアプローチでは、__noSuchMethod__の仕掛けを求めるオブジェクトのprototypeチェーンが終わる直前にMethodSinkを挿入しなければならない。ここで使われる__proto__デファクトスタンダードは、ES6でデジュールスタンダードになるだろう。Object.prototype.__noSuchMethod__によるバックストップは、バグをキャッチするためにMethodSinkがrecieverのprototypeチェーンではなかったところでスロー(throws)する。
この実装は上のobj.bar()以下にあるように、存在しないメソッドを呼び出したときに__noSuchMethod__フックを呼び出すだけではない。ターゲットオブジェクトになく、object.prototypeにないプロパティのどんなgetのためのthunkを作り出す。
js> obj.bar
(function (...args) {
return receiver.__noSuchMethod__(name, args);
})
js> var thunk = obj.bar
js> thunk()
"bar"