Symbol値(=symbol)は、JavaScriptのプリミティブ値(プロパティやメソッドを持たない値)の1つである。最大の特徴は、同じ値を作れない点にある。

また、他のプリミティブ値と違い、ソースコードに直接書いて表せない。

Symbol値とその他のプリミティブ値の作成方法

以下、JavaScriptにおけるプリミティブ値とその作成方法の一覧である。

種類 作成方法
symbol Symbol()関数を使う。
string 'あるいは"あるいは`で囲って書くか、 String()関数を使う。
number 数字を直接書くか、Number()関数を使う。
bigint nで終わる数字を直接書くか、BigInt()関数を使う。
boolean truefalseを直接書くか、Boolean()関数を使う。
undefined undefinedを直接書く。
null nullを直接書く。

Symbol値だけ、直接書く形で作成することができない。これはSymbol()関数を呼び出すたびに、Symbol値に対して一意の値を付与し、一意性を確保していることによる。

String('x') == String('x')trueだが、Symbol('x') == Symbol('x')falseとなる。

プリミティブ値とそのラッパーオブジェクト

undefinednullを除き、プリミティブ値はそれぞれSymbolStringNumberBigIntBooleanというオブジェクトに対応しているように見える。

実際は、プリミティブ値とこれらのオブジェクトは別物である。プリミティブ値に対してメソッドやプロパティを使おうとすると、JavaScriptは自動的にプリミティブ値をラップする。そして、そのラッパーオブジェクトのメソッドやプロパティが使われる。この仕組みをAuto-boxingという。

初期のJavaScriptはAuto-boxingに対応しておらず、プリミティブ値のラッパーオブジェクトはコンストラクタで生成されていた。らしい。そのためStringNumberBooleanの古い3つは、newキーワードなしだとプリミティブ値、ありだとラッパーオブジェクトを返す仕様になっている。

これは冗長であるので、SymbolBigIntでは、Auto-boxingのみに一本化された。そのためSymbol()BigInt()newキーワードをつけると、「おれ様はコンストラクタじゃねぇ」と怒られる。

Symbol(あとBigInt)はnewをつけては作れない。要注意である。

Symbolとsymbol

Symbolが何なのか混乱してきた。整理する。

Symbolは、関数として呼び出すとsymbolというプリミティブ値を返す組み込みオブジェクトである。MDNのSymbolの解説ではSymbol constructorとも呼ばれている。その割にnewをつけるとUncaught TypeError: Symbol is not a constructorみたいなエラーが出る。

謎。

何か誤解しているのか。Symbolファクトリとかの方がわかりよくないだろうか。とりあえず以降は素直にSymbolコンストラクタと呼ぶ。

Symbolコンストラクタに渡す引数

Symbolコンストラクタには引数が渡せる。この引数は文字列とされているが、オブジェクトも数値も真偽値も渡せる。ただしいずれも自動で文字列に変換される。

この引数は、descriptionというプロパティアクセサを介してsymbol値に保持される。これは、デバッグ時にシンボルを識別するために用いられる。

Symbol値の使いどころ

Symbol値を使ったプロパティは、通常のキーとは異なる働きをする。具体的には、for...inループ、Object.keys()メソッド、Object.getOwnPropertyNames()メソッドなどで参照できない。

そのためSymbol値の主な使いどころとしては、オブジェクトのプロパティを隠したい(不用意に上書きされたくない)場合や、メタデータを付与したいケースが挙げられる。

Well-known Symbolsとは

Symbol値の使い方は、Symbolコンストラクタそのものが体現している。

Symbolコンストラクタの静的プロパティはすべてSymbol値で定義されている。それらは、StringArrayRegExpといった組み込みオブジェクトの、一部のプロパティやメソッドのキーとして使われている。

こういった組み込みオブジェクトに使われるシンボルは、ウェルノウン・シンボルと呼ばれている。組み込みオブジェクトのキーについて、シンボルと文字列がどのように使い分けられているかは知らない。単純に古い、新しいの問題ではないようだ。分かったら追記したい。

ウェルノウン・シンボルは、ドキュメント上では@@symbol_nameというように表現される。Array.prototype[@@iterator]()のような形である。一覧はここあるいはここで確認できる。

Symbolコンストラクタの静的メソッド

Symbolコンストラクタには、以下の2つの静的メソッドがある。

メソッド名 概要
for(文字列) グローバルシンボルレジストリを検索し、引数に渡された文字列がキーと一致するシンボルを返す。なければ文字列をキーにして、新たなシンボルを作って返す。
keyFor(シンボル) グローバルシンボルレジストリを検索し、引数に渡されたシンボルがあれば、そのシンボルに対応するキーを返す。設定されていなければundefinedを返す。

グローバルシンボルレジストリって何

MDNのドキュメントを呼んでいたらいきなりグローバルシンボルレジストリとかランタイムワイドシンボルレジストリとか言われて面食らった。

これは、複数の実行環境でsymbol値を共有するための名前空間のようなものである。

symbol値は、1つの実行環境ですら重複を許さない。そのため他のプリミティブ値のように、手軽に別の実行環境(iframeなど)へ渡すことができない。重複がないことを確認する仕組みが必要だ。

その仕組みがグローバルシンボルレジストリである。ここで、JavaScriptエンジンが全体のsymbol値を管理する。

グローバルシンボルレジストリの操作は、Symbol.for()メソッドとSymbol.keyFor()メソッドで行う。

ローカルのSymbol()コンストラクタと違い、グローバルシンボルレジストリではSymbol.for()メソッドに渡す文字列をキーにしてシンボルが保存される。このキーは、descriptionアクセサプロパティとは別物である。

分かったようなわからないような……やっぱりわからない。

まとめ

  • Symbolは唯一無二の文字列みたいなもの。
  • Symbolには3種類ある(ローカル、ウェルノウン、グローバル)。
  • ローカルはSymbol()関数でつくる。
  • ウェルノウンはSymbol.iteratorみたいに使う。
  • グローバルはSymbol.key()メソッドで作る。

参考資料