ECMAScript6 (ES6, ES2015) で導入された新機能の大半は、読めば便利さが想像くらいはできるけど、自分にとってしばらく訳がわからなかった新機能が「シンボル」(MDNのリファレンス)。これまで文字列だけで上手くやってきたJavaScriptに、急になぜこんなものが導入されたの? イマイチ分からなかったので、調べたことのまとめ。
シンボルの簡単なおさらい
知ってる人は読み飛ばし推奨。詳しく見たい人は、こことかの他のページを参照。
- ES6で導入された、
Symbol()
で作成できる特殊な値。 -
Symbol()
は、 typeof すると'number'
でも'string'
でも'object'
でもなくて'symbol'
となる、まったく新しいタイプのプリミティブ値を生成する。 - 一旦作ったシンボルは、それ自身とのみ等しくなる、ユニークなIDとして機能する。
===
や==
で評価して true になるものを、後から再生成することはできない。そういう意味ではオブジェクトに似ている1。 - シンボルはオブジェクトのプロパティキーとして使用できる。そういう意味では文字列に似ている。つまり
var foo = Symbol(); hoge[foo] = 5;
というコードが動作するが、これはhoge.foo = 5
やhoge['foo'] = 5
とは全く別物。 - シンボルをキーとしてオブジェクトに保存された値は、一般的な
for ... in
ループで列挙されない。作成したシンボル(つまり上記のfoo
)をスコープの外に出るなどして忘れてしまったら、既存の方法で値を参照することは二度と不可能になる2。 - ES6で導入された新機能の幾つかが、この仕組みに依存している。イテレーターを例にとると、ES6のランタイムはオブジェクト
a
がfor ... of
ループで使えるかどうかを判定するために、a['iterator']
という文字列名のメソッドが存在するかを調べるのではなく、a[Symbol.iterator]
という定義済みシンボルをキーに持つメソッドが存在するかどうかを調べる。
理由は互換性
というわけで本題。「仕組み自体は難しくないけど、これって今更JavaScriptに導入が必要だったの?」
シンボルがES6に導入された最大の理由は互換性のよう。つまり、オブジェクトに、既存プログラムに影響せずに「特殊メソッド」みたいなものを安全に作れるようにしたかったから。
太古の昔から認知されている特殊メソッドである toString()
とかは今更なのでいいとしても、この時代に突然「本日以降 iterator()
は特殊なメソッド名になったのでJavaScriptエンジンが勝手に呼び出します、こうやって実装してください」とか宣言されるのは困る。そんな名前の通常メソッドは既に世の中にありふれているわけで、ある日を境に勝手にランタイムから呼ばれるようになったら壊れてしまう。
JavaやC#のように「Iterable
インターフェースを実装してね」とは言えない。インターフェースなんてない。PHPのように「 __
で始まるメソッド名は将来の特殊関数名のために予約しています」などとドキュメントで宣言しておけばよかったけど、JSにはそういう決まりもなかった。特殊メソッドの名前が obj.__getIterator__()
だろうと obj.$iterator()
だろうと、やはり世界のどこかのJSプログラムが壊れる。メソッド名を obj.__ES6$$NEW$$_$iterator$()
とか、もういっそのこと obj['http://namespace.ecma-international.org/ecma-262/6.0/methods#iterator']()
とかにすれば事実上被らないけど、こんなメソッド名はイヤすぎるし、これそのものが for ... in
で列挙されないようにするためにES5では大変面倒な記法が必要になる。
JavaScriptの実行環境は世界中で勝手にどんどんアップデートされていく。互換性が失われた場合、今まで動いていたプログラム(少なくとも、行儀良く書かれていたもの)がブラウザの更新と同時にある日突然壊れる可能性がある。これがPythonとかの他の言語であれば、メジャーバージョンアップで多少の非互換を導入しても、ライブラリが対応するまで2.xでじっと待つ、という選択肢もありえるが、JavaScriptでそれはできない。標準クラスに特殊メソッドを足して世界中の既存アプリがある日突然壊れうる状況は防がないといけない。
そんなわけでシンボルが導入された。シンボルを使えば、ES6より前に作られたどんなライブラリも壊すことなく、JavaScriptに特殊メソッドを足せる。乱暴な言い方をすると、JavaScriptにおけるシンボルは、PHPのマジックメソッドと同じようなものを後付けで言語仕様に導入する仕組み、と言える。
ES6 では well-known symbols が多数定義されているが、これらはPHPやPythonといった言語での「__
で始まる特殊メソッド」に意味合いとして似ている。これらは for ... of
ループや instanceof
の動作をオーバーライドしたりするために使う「メソッド名」のようなものだが、既存のライブラリがメソッド名に何を使っていても誤動作の心配はない。というか既存の手段では存在すら知るすべがない。
var fib = {
iterator: function() { /* これは文字列キーなので普通の関数 */ },
[Symbol.iterator]: function*() {
/* これはランタイムから呼ばれる特殊な関数 */
let i = 0, k = 1;
yield k;
while(k < 100) {
k = i + k;
i = k - i;
yield k;
}
}
};
for (let k in fib) console.log(k); // iterator (だけ)
for (let k of fib) console.log(k); // 1, 1, 2, 3, ... , 144
めでたしめでたし。
自前でシンボルを使うべきか?
そんな訳で、ECMAScriptの仕様策定側にとってシンボルは非常に重要。だが逆に言えば、使うライブラリを自分達で決められ、自分達で書いたアプリを自分達でテストできる一般の開発者は、名前の衝突なんてどうとでも回避してきたわけで、今更シンボルが使えてもそんなに嬉しいことはないと思われる。
今のところ自分が思いつくのは以下のような使い方。
列挙・定数
例えば enum や const のようなものは、シンボルを使って以下のように書けば気持ちがいい。
const LOG_LEVEL_INFO = Symbol('log_level_info');
const LOG_LEVEL_WARN = Symbol('log_level_warn');
var suits = {
HEART: Symbol('heart'),
DIAMOND: Symbol('diamond'),
SPADE: Symbol('spade'),
CLUB: Symbol('club')
};
実体がシンボルであって文字列や数字でないため、 loglevel === 'info'
とか suit === 3
とかリテラルで書いてサボれず、必ず該当モジュールをインポートして enum(っぽいもの)を使用するという手順をユーザに強制でき、コードのメンテ性が高まる。
これの最大の欠点はJSONと相性が悪いこと。現状、JSON.stringify()
を普通に使うとシンボル(キー側にあっても値側にあっても)が完全に無視されてしまう。今でも第二引数のreplacerで頑張れば部分的にはなんとかできるが、個人的にはまだこういう使い方はしたくない。
DOMにプライベート情報を保存
ライブラリ作者などがDOM要素に一時データを保存したい場合、これまでは他のライブラリとの名前の衝突を防ぐため、 __mylib_private_isHogehoge
のような、他のライブラリと干渉しないであろうユニークなキー名を使うなどしてしのいでいたが、シンボルを使えばそれがエレガントになる。
// こうする代わりに
var isChecked = 'mylib_private_isChecked_j8z54eomjlcz';
domElement[isChecked] = true;
// シンボルを使えば、絶対に他人と衝突しない
var isChecked = Symbol('isChecked');
domElement[isChecked] = true;
なおプライベートと書いたけど、真に秘密の情報を保存できるわけではないので注意2。
ビルトインクラスの安全な拡張
おそらく一番有用そうな使い方がこれ。汎用ライブラリが Array
とか Date
とかのビルトインクラスのprototypeを(polyfill目的以外で)いじって拡張することは、「プロトタイプ汚染」と呼ばれ、ほぼタブーに近いバッドプラクティスと見なされてきた。例えば Array.prototype.shuffle()
や Object.prototype.toJSON()
のようなメソッドを個々のライブラリが好き勝手に生やすと、他のライブラリと名前が被ったり、将来標準JavaScriptに仕様の違う同名メソッドを定義されたりして、目も当てられない…という話3。
だがシンボルを使うと「同名メソッドの競合」という事態はそもそも起きようがないので、常識が変わる可能性がある。
var _ = require('underpoint'); // という仮想ライブラリ
var $ = require('Qjuery'); // という仮想ライブラリ
var array = [1, 2, 5, 8, 13];
// Arrayに生えた2つのshuffleの実装は決して衝突しない
var shuffled1 = array[_.shuffle]();
var shuffled2 = array[$.shuffle]();
// 将来標準のshuffleが実装されても問題なし
var shuffled3 = array.shuffle();
// _.each(_.map(_.filter([2,5,7], x => x % 2), x => x * 2), alert)
// も chain() を使わずにチェインできる
[2,5,7]
[_.filter](x => x % 2)
[_.map](x => x * 2)
[_.each](alert);
プロトタイプ汚染を避けるために $.each(array, callback)
みたいなネストが深くなる書き方を強いられていた人類には朗報(タイプ数はちょっと増えてるけど)。
2018年追記:2015年に上記のようなことを書いてから3年、別に上記のような混沌は発生せず。関数のカッコが深くなる問題についてはパイプライン演算子と部分適用でよりエレガントに処理する方向が有望になった。結局、イテレータを扱う時以外はシンボルの事は忘れてて良さげ。
そんなわけで、近い将来、シンボル使いまくり派と、これまで通り文字列でいいじゃん派とが混在し、またJavaScriptの書き方に新たな混沌がもたらされる、かもしれない。