Symbol とは:必ず一意になる値
Symbol は JavaScript のプリミティブ型のひとつで、string、number、boolean、null、undefined、bigint と並ぶ仲間です。特徴はいたってシンプルで、作成した Symbol はそれ以降どんな Symbol とも絶対に一致しない、という点にあります。
引数なしで作った Symbol 同士でも、やはり等しくならない。これは偶然ではなく、むしろ Symbol の存在意義そのものです。偽造することも、うっかり同じものを作り直すことも、他人が作った Symbol と衝突することもできません。
デバッグ用に説明文(description)を付けることもできますが、これは識別には一切影響しません。
説明文が同じでも、生成される Symbol は別物です。description はあくまで、ログを読む人間向けのラベルでしかありません。
Symbol が存在する理由:衝突しないキーを作るため
そもそも JavaScript に Symbol があるのは、他人が使っているキーと被る心配をせずにオブジェクトへプロパティを追加できるようにするためです。文字列をキーにすると、全員がフラットな名前空間を取り合うことになります。たとえば 2 つのライブラリがどちらも obj.meta にメタデータを書き込もうとしたら、一方が他方を上書きしてしまいますよね。Symbol キーならこの心配はありません。あなたが作った Symbol は、他の誰も持っていないからです。
[ID]: 42 の角かっこは computed property(計算プロパティ)の構文で、「ID の値をキーとして使う」という意味になります。同じ ID シンボルを持っているコードだけが、このプロパティを読んだり書き換えたりできるわけです。他のモジュールが "id" という文字列を使ったり、自分のところで Symbol("id") を作ったりしていても、それらはまったく別のスロットを指していることになります。
Symbol をキーにしたプロパティはほぼ隠れる
Symbol をキーにしたプロパティは、for...in や Object.keys、JSON.stringify には現れません。秘密というわけではなく、本気で探せば見つけられますが、通常のイテレーションでは邪魔にならないところに収まってくれます。
Object.keys や JSON.stringify はシンボルキーを完全にスキップします。シンボルプロパティを意図的に取得したいときは、Object.getOwnPropertySymbols や Reflect.ownKeys を使えば見えるようになります。これはメタデータに理想的な挙動で、探しにいけば見つかるけれど、文字列キーをただ走査するだけのコードからは見えない、というわけです。
Symbol.for でシンボルを共有する
Symbol() で作られるのは、その場限りのローカルなシンボルです。一方で、モジュールをまたいでもプログラム全体で_同じ_シンボルを参照したい、つまり複数の場所のコードが同じキーで合意したい、というケースもあります。そのために用意されているのが Symbol.for です。
Symbol.for(key) はグローバルレジストリを参照します。同じキーのシンボルが既に登録されていればそれを返し、無ければ新しく作って登録します。逆方向の Symbol.keyFor は、登録済みのシンボルから元のキー(文字列)を取り出すためのもので、登録されていないシンボルを渡すと undefined が返ります。
基本的には、普段使いには素の Symbol() で十分です。Symbol.for の出番は、モジュールの境界をまたいで同じシンボルを共有したい、といった場面に限られます。
Well-known symbols:言語仕様へのフック
シンボルが本領を発揮するのはここからです。JavaScript には well-known symbols と呼ばれる定義済みシンボルが用意されていて、言語処理系そのものがあなたのオブジェクトに対してこれらのキーを見にきます。つまり、このキーに対応するメソッドを実装するだけで、自分のオブジェクトを組み込み機能にそのまま差し込めるわけです。
もっともよく使うのが Symbol.iterator です。このキーにメソッドを持つオブジェクトは イテラブル とみなされ、for...of、スプレッド構文、分割代入、Array.from などがそのまま動くようになります。
range は配列ではありません。特別なキーを持ったメソッドを1つだけ備えた、ただのオブジェクトです。それでも、そのメソッドがイテレータを返すおかげで、for...of もスプレッド構文もそのまま使えます。配列や文字列、Map と同じ感覚で扱えるわけです。これが言語側との「契約」。つまり Symbol.iterator さえ実装すれば、JavaScript はそのオブジェクトをイテラブルとして認めてくれます。
覚えておきたい Well-Known Symbols
他にもいくつかあり、それぞれが言語の異なる部分へのフックになっています。
Symbol.iterator— オブジェクトを反復可能(iterable)にするためのシンボル。Symbol.asyncIterator— 上と同じだが、for await...of用。Symbol.toPrimitive— オブジェクトがプリミティブ(数値・文字列・デフォルト)に変換されるときの挙動を制御する。Symbol.hasInstance—instanceofが自作クラスに対して返す結果をカスタマイズする。Symbol.toStringTag—Object.prototype.toString.call(obj)で表示されるタグを設定する。
すべて暗記する必要はありません。「こういうものが用意されている」と知っておくだけで十分です。自作オブジェクトを組み込み型のように振る舞わせたくなったとき、たいていそれに対応する well-known symbol が存在します。
Symbol は文字列ではない
よくあるハマりどころとして、Symbol は文字列に自動変換されません。そのまま文字列連結しようとするとエラーになりますし、文字列を期待する API に渡してもやはり例外が飛びます。
テンプレートリテラルでも同じ TypeError が発生します。文字列として扱いたいときは String(symbol) や symbol.toString() を使ってください。この扱いにくさは意図的なもので、一意な識別子である値を「ただの文字列データ」として誤って扱ってしまわないよう、言語側が守ってくれているのです。
Symbol を使うべき場面と避けるべき場面
次のようなケースでは Symbol の出番です。
- 自分が所有していないオブジェクトにメタデータを付けたいとき。衝突しないキーが必要になります。
- プロトコルを設計するとき。「自分のライブラリと連携したいオブジェクトは、この Symbol をキーにメソッドを実装してね」という使い方です。
JSON.stringifyやfor...inに出てこないプロパティが欲しいとき。Symbol.iteratorなどの well-known symbols を実装するとき。
一方、次のような場合は Symbol を使わない方がよいでしょう。
- 単にオブジェクトのキーが欲しいだけで、衝突の心配もないとき。文字列の方がシンプルで、ログにもそのまま表示されます。
- 「プライベートなフィールド」として使いたいとき。Symbol をキーにしたプロパティは厳密には非公開ではなく、
Object.getOwnPropertySymbolsで見えてしまいます。本当にプライベートにしたいなら、クラスの#privateフィールドを使いましょう。 JSON.stringifyを通しても残ってほしいデータを保存したいとき。Symbol キーのプロパティは消えてしまいます。
実際、ほとんどの JavaScript コードは Symbol(...) を直接書かずに成立します。ただし、自作のオブジェクトを for...of で回したくなったり、型変換の文脈で自然に振る舞わせたくなった瞬間に、Symbol はその内部機構への扉になってくれます。
次回: 関数宣言
Symbol、イテレータ、ジェネレータは、いずれも関数に深く依存しています。Symbol.iterator に格納するメソッド、イテレータを返すファクトリ関数、そして定型処理の大半を隠してくれる function* 形式のジェネレータなど。次章のテーマは関数です。まずはいくつかある宣言方法と、それぞれで何が変わってくるのかを見ていきます。
よくある質問
JavaScriptのSymbolって何ですか?
Symbolは、値の一意性が保証されたプリミティブ型です。Symbol() や Symbol('description') で生成でき、何回呼んでも同じ値は返ってきません。主な用途は、他のキーと衝突しないオブジェクトキーとして使うこと、そして Symbol.iterator のようなWell-knownシンボル経由で言語機能にフックすることです。
文字列キーじゃなくてSymbolを使うべき場面は?
他のコードと名前が被らずにプロパティを追加したいときですね。たとえばライブラリがメタデータを付けたい、フレームワークがオブジェクトにタグ付けしたい、あるいは公開APIにはしたくない内部用キーを定義したい、といったケースです。普段の読みやすさ重視のキーで衝突の心配がないなら、素直に文字列で十分です。
Symbol.iteratorは何のためにあるの?
Symbol.iterator はWell-knownシンボルのひとつで、for...of やスプレッド構文、分割代入に「このオブジェクトはこうやって反復する」と教える役割を持ちます。Symbol.iterator をキーにしてイテレータを返すメソッドを定義すれば、そのオブジェクトはイテラブルになります。配列や文字列、Map、Set が反復できるのも、内部でこの仕組みを使っているからです。
Symbol()とSymbol.for()の違いは?
Symbol('x') は呼ぶたびに新しいSymbolを作ります。説明文字列が同じでも別物扱いです。一方 Symbol.for('x') はグローバルレジストリを参照し、同じキーならプログラム全体で同じSymbolを返します。モジュールやrealmをまたいで共有したいときは Symbol.for、ローカルでユニークにしたいだけなら Symbol() を使いましょう。