2015-02-07

ES6 Symbolとはなんなのか

なんか最近WebKit (JSC) に ES6 Symbol が実装されたようなんですが、Symbol とかそんなのあったのか……というレベルだったので調べてみました。調べてみたらけっこう知らない世界でした。ES6、いつのまにかこんなことができたのかって感じ。

シンボルとは何なのか

プログラミング言語におけるシンボルというのは……名前がついて区別可能なモノ、というのが一般的な理解である気がします。LISPでは多用されますね。たとえばメソッドの名前とか構造体のフィールド名、連想配列のキーやenumのようなものとして使ったりします。Rubyでもおなじみです。

まあこういう説明が必要な人はあんまりここを読んでない気もするのでwikipediaへのリンクで済ませたい。

ES6のシンボル

ES6のシンボルも似たような感じなんですが、いろいろ違うところもあって戸惑います。以下はおおむねMDNからもってきたものですが……

まず、シンボルは Symbol() で作ります。

a = Symbol()

引数に文字列も渡せます。

b = Symbol("foo")

ただ、この文字列はいわゆるシンボル名ではありません。

a == a // => true
a == b // => false
b == Symbol("foo") // => false

Symbol()が呼ばれるたびに毎回あたらしくシンボルが作られるという感じです(gensymと同じですかね)。Symbol()に渡す引数は仕様ではdescriptionと呼ばれています。

もちろん特定の名前と関連付けられたシンボル、というのも作ることができます。Symbol.forというのがそれです。Symbol.keyForによって名前を逆引きできます。

c = Symbol.for('foo')
c == Symbol.for('foo')  // => true
b == c  // => false
Symbol.keyFor(c)  // => 'foo'
Symbol.keyFor(b)  // => undefined

んでシンボルの用途ですが、オブジェクトへのキーにできます。

o = {}
o[a] = 1
o[a]  // => 1

まあ文字列と一緒ですね。ただし、文字列化しているわけではありません。

o[a.toString()]  // => undefined

そしてなぜか、oのキーとして、シンボルは通常見えなくなります。たとえばObject.keys()やfor...in構文などでは無視されるようになります。

Object.keys(o)  // => []
o[a]  // => 1

JSON.stringifyでも無視されます。

JSON.stringify(o)  // => "{}"

いちおう「キーのうちシンボルだけ」を取り出すAPIもあるので頑張れば持ってこれますが、基本的には見えてこないということですね。

ES6 シンボルの用途

さてこれなんに使うんだろうか……と不思議だったんですが、プライベートなフィールドを作るのに便利そうです。http://tc39wiki.calculist.org/es6/symbols/ の例……はモジュールを使ってますが、ふつうのJSっぽく書くと、

var Foo;
(function() {
  var sym = Symbol('foo');
  Foo = function() {
    this[sym] = 'foo';
  }
  Foo.prototype.bar = function() {
     // do something with this[sym]...
  }
})()

こんなふうに書くと、symのフィールドには外部からはわりと不可視になります。外部から不用意にアクセスできないようなフィールドには便利ですね。

もう一つとして、ある種の構文を提供できるようになるようです。というかたぶんこれが主な用途なんですかね。

たとえばSymbol.iteratorというシンボルが定義されていますが、これによって任意のオブジェクトを for...of構文に渡したりできます。

function Range(s, e) {
  this.start = s;
  this.end = e;
}
Range.Iterator = function(range) {
  this.range = range;
  this.current = range.start;
}
Range.Iterator.prototype.next = function() {
  if (this.current >= this.range.end)
    return {done: true};
  return {value: this.current++, done: false};
}

Range.prototype[Symbol.iterator] = function() {
  return new Range.Iterator(this);
}
for (var x of new Range(4, 10)) { console.log(x); }  // => 4, 5, 6, 7, 8, 9

こういう特殊なシンボル(Well-known symbols)は現行のES6において11個定義されているみたいですが、Chromeにはまだiteratorしかないようです。