2021年11月24日

シンボル型

仕様によると、オブジェクトのプロパティのキーは文字列型、もしくはシンボル型のいずれかです。数値ではなく、真偽値でもなく、文字列またはシンボル、それら2つの型だけです。

私たちはこれまで文字列だけ見てきました。今シンボルが我々に与えてくれるアドバンテージを見てみましょう。

シンボル

“シンボル” 値はユニークな識別子を表現します。

このタイプの値は、Symbol() を使って作ることができます:

// id は新しい symbol です
let id = Symbol();

また、シンボルに説明を与えることもでき(シンボル名と呼びます)、デバッグ目的で便利です。

// id は "id" という説明を持つ symbol です
let id = Symbol("id");

シンボルはユニークであることが保証されます。たとえ同じ説明で複数のシンボルを作ったとしても、それらは異なる値です。説明は何にも影響を与えない単なるラベルです。

例えば、ここでは同じ説明をもつ2つのシンボルがあります – これらは等しくありません:

let id1 = Symbol("id");
let id2 = Symbol("id");

alert(id1 == id2); // false

Rubyもしくは他の言語にも慣れ親しんでいる人は、 – 間違ってはいけません。 JavaScriptのシンボルは異なります。

Symbols は文字列への自動変換はしません

JavaScriptにおいて、ほとんどの値は文字列への暗黙の変換をサポートしています。例えば、任意の値で alert を呼びだすと、たいていの値は動作します。が、シンボルは特別です。それらは自動変換されません。

例えば、この alert はエラーになります:

let id = Symbol("id");
alert(id); // TypeError: Cannot convert a Symbol value to a string

これは、文字列とシンボルは根本的に異なるものであり、誤って別の文字列に変換してはいけないため、混乱を避けるための “言語によるガード” です。

もし本当にシンボルを表示したい場合は、このように toString() を呼ぶ必要があります:

let id = Symbol("id");
alert(id.toString()); // Symbol(id), これは動作します

あるいは、symbol.description プロパティを利用して、説明のみを表示します:

let id = Symbol("id");
alert(id.description); // id

“隠れた” プロパティ

シンボルを使うと、オブジェクトの “隠れた” プロパティを作ることができます。他のコードがアクセスしたり上書きしたりすることはありません。

例えば、サードパーティのコードに属する user オブジェクトを使用している場合です。 それらに “識別子” を追加したいとします。

シンボルのキーを使用してみましょう:

let user = { // 別のコードに属しているオブジェクト
  name: "John"
};

let id = Symbol("id");

user[id] = 1;

alert( user[id] ); // キーとして symbol を使ってデータにアクセスできます

文字列 "id" に対して Symbol("id") を使うことの利点は何でしょうか?

user オブジェクトは別のコードに属しており、コードはそこでも動作するので、そこに単純に任意のフィールドを追加するべきではありません。それは安全ではありません。しかし、シンボルは誤ってアクセスすることはできず、サードパーティのコードは恐らく見ることすらできないため、恐らく問題になりません。

また、別のスクリプトが、独自の目的のために自身の “id” プロパティを user の中に持ちたいとします。それは別のJavaScriptライブラリの場合もあり、スクリプトは完全にお互いを認識していない状況とします。

そして、そのスクリプトは自身の Symbol("id") を作ります。:

// ...
let id = Symbol("id");

user[id] = "Their id value";

たとえ同じ名前でもシンボルは常に異なるため、衝突は起こりません。

ですが、もし同じ目的のためにシンボルの代わりに文字列 "id" を使ったとすると、衝突が発生する かもしれません

let user = { name: "John" };

// 我々のスクリプトは "id" プロパティを使います
user.id = "ID Value";

// ...もし後で別のスクリプトが別の目的で "id" を使ったら...

user.id = "Their id value"
// boom! 上書きされます! 同僚に危害を加えるつもりはありませんでした。が、してしまいました!

リテラルのシンボル

オブジェクトリテラルの中でシンボルを使いたい場合は、角括弧で囲む必要があります。

このように:

let id = Symbol("id");

let user = {
  name: "John",
  [id]: 123 // 単に "id: 123" ではありません
};

キーとして、変数 id の値が必要であり、文字列 “id” ではないからです。

シンボルは for…in ではスキップされます。

シンボリックなプロパティは for..in ループに参加しません。

例:

let id = Symbol("id");
let user = {
  name: "John",
  age: 30,
  [id]: 123
};

for (let key in user) alert(key); // name, age (no symbols)

// symbol による直アクセスは動作します
alert( "Direct: " + user[id] );

Object.keys(user) もそれらを無視します。これは一般的な “隠れている” というコンセプトの一部です。もし別のスクリプトかライブラリが我々のオブジェクトをループした際に、予期せずシンボリックプロパティにアクセスすることはありません。

一方で、Object.assign は文字列とシンボルプロパティ両方をコピーします;

let id = Symbol("id");
let user = {
  [id]: 123
};

let clone = Object.assign({}, user);

alert( clone[id] ); // 123

ここにはパラドックスはありません。それはデザインによるものです。考え方としては、我々がオブジェクトをクローンしたりマージするとき、通常 全ての プロパティがコピーされることを望むであろうということです( id のようなシンボルも含めて)。

グローバルシンボル

これまで見てきたように、通常はたとえ同じ名前であったとしてもすべてのシンボルは異なります。しかし、時には同じ名前のシンボルを同じエンティティにしたいときがあります。例えば、我々のアプリケーションの異なる部分が、正確に同じプロパテイを意味するシンボル "id" にアクセスしたいとします。

それを達成するために、グローバルシンボルレジストリ があります。その中でシンボルを作り、後でそれらにアクセスすることができます。同じ名前への繰り返しアクセスは、まったく同じシンボルを返すことが保証されます。

レジストリからシンボルを読み取る(ない場合は作成する)ためには、Symbol.for(key) を使います。

この呼び出しはグローバルレジストリをチェックし、key として記述されたシンボルが存在する場合にはそれを返しますが、そうでなければ新しいシンボル Symbol(key) を作成し、与えられた key で、レジストリ内に格納します。

例:

// グローバルレジストリから読む
let id = Symbol.for("id"); // symbol が存在しない場合、作られます

// 再度読み込み
let idAgain = Symbol.for("id");

// 同じシンボル
alert( id === idAgain ); // true

レジストリ内のシンボルは グローバルシンボル と呼ばれます。コード内のどこからでもアクセス可能なアプリケーション全体のシンボルが必要な場合、これを使います。

これは Ruby のようです

Rubyのようないくつかのプログラミング言語では、名前毎に1つのシンボルがあります。

JavaScriptでは、ご覧の通りそれはグローバルシンボルのことです。

Symbol.keyFor

グローバルシンボルでは、Symbol.for(key) は名前によってシンボルを返すだけでなく、逆方向の呼び出しもあります:Symbol.keyFor(sym), これは逆のことをします: グローバルシンボルを元に、名前を返します。

例:

// 名前 から シンボルを取得
let sym = Symbol.for("name");
let sym2 = Symbol.for("id");

// symbol から名前を取得
alert( Symbol.keyFor(sym) ); // name
alert( Symbol.keyFor(sym2) ); // id

Symbol.keyFor は内部ではそのシンボルのキーを探すためにグローバルシンボルレジストリを使っています。従って、非グローバルのものに対しては動作しません。もしシンボルが非グローバルの場合、見つけることはできず、undefined を返します。

とは言え、どのシンボルも description プロパティを持っています。

例:

let globalSymbol = Symbol.for("name");
let localSymbol = Symbol("name");

alert( Symbol.keyFor(globalSymbol) ); // name, グローバルシンボル
alert( Symbol.keyFor(localSymbol) ); // undefined, グローバルではないので

alert( localSymbol.description ); // name

システムシンボル

JavaScriptが内部的に使用する多くの “システム” シンボルが存在し、それを使うことでオブジェクトの様々な側面を微調整することができます。

それらはよく知られているシンボル テーブルの仕様にリストされています。

  • Symbol.hasInstance
  • Symbol.isConcatSpreadable
  • Symbol.iterator
  • Symbol.toPrimitive
  • …等.

例えば、Symbol.toPrimitive はオブジェクトからプリミティブへの変換を記述することができます。私たちはすぐにそれを使うところを見ていきます。

他のシンボルについても、該当する言語の機能を学ぶときに分かるようになるでしょう。

サマリ

Symbol はプリミティブ型で、ユニークな識別子のためのものです。

シンボルはオプションの記述と合わせて呼ばれる Symbol() で生成されます。

シンボルは、たとえ同じ名前を持ったとしても常に異なった値です。もし同じ名前のシンボルを同じものにしたいなら、グローバルレジストリを使う必要があります: Symbol.for(key) は名前として key をもつグローバルシンボルを返します(必要なら作ります)。Symbol.for の複数回の呼び出しは全く同じシンボルを返します。

シンボルは2つの主なユースケースがあります:

  1. “隠れた” オブジェクトのプロパティ。 もし別のスクリプト、またはライブラリに “属している” オブジェクトにプロパティを追加したい場合、シンボルを作り、プロパティのキーとしてそれを使うことができます。シンボリックなプロパティは for..in には現れないため、リストされることはありません。また、直接アクセスされることもありません。なぜなら、別のスクリプトは我々のシンボルを持っていないため、そのアクションに介入することはできません。

    従って、シンボリックプロパティを使うことで、必要なオブジェクトに何かを “こっそり” 隠すことができます。そしてそれは他人には見えません。

  2. Symbol.* としてアクセス可能なJavaScriptで使われている多くのシステムシンボルがあります。いくつかの組み込みの振る舞いを変更するためにそれらを使うことができます。例えば、チュートリアルの後半でiterablesのための Symbol.iterator, object-to-primitive conversionを設定するための Symbol.toPrimitive などを使います。

技術的には、シンボルは100%隠れる訳ではありません。全てのシンボルを取得する組み込み関数Object.getOwnPropertySymbols(obj) があります。また、シンボリックなものも含めてオブジェクトの 全ての キーを返すReflect.ownKeys(obj)と呼ばれる関数もあります。なので、それらは本当に隠れてはいません。しかしほとんどのライブラリや組み込み関数、構文構造は共通の合意に忠実です。そして、前述の方法を明示的に呼び出す人は、それらがやっていることをよく理解しているでしょう。

チュートリアルマップ