2022年6月27日

シンボルの種類

仕様では、オブジェクトのプロパティキーとして使用できるプリミティブ型は2つだけです。

  • 文字列型、または
  • シンボル型。

それ以外の場合、数値などの別の型を使用すると、文字列に自動的に変換されます。そのため、 `obj[1]` は `obj["1"]` と同じであり、 `obj[true]` は `obj["true"]` と同じです。

これまでは文字列のみを使用していました。

次に、シンボルについて調べて、シンボルで何ができるかを見てみましょう。

シンボル

「シンボル」は一意の識別子を表します。

この型の値は、 `Symbol()` を使用して作成できます。

let id = Symbol();

作成時に、シンボルに説明(シンボル名とも呼ばれます)を付けることができます。これは主にデバッグの目的で役立ちます。

// id is a symbol with the description "id"
let id = Symbol("id");

シンボルは一意であることが保証されています。まったく同じ説明で多くのシンボルを作成しても、それらは異なる値です。説明は、何も影響を与えない単なるラベルです。

たとえば、ここでは説明が同じ2つのシンボルがありますが、それらは等しくありません。

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

alert(id1 == id2); // false

Ruby やその他の「シンボル」のようなものを持つ言語に精通している場合は、誤解しないでください。 JavaScript のシンボルは異なります。

つまり、要約すると、シンボルはオプションの説明が付いた「プリミティブな一意の値」です。それらをどこで使用できるかを見てみましょう。

シンボルは文字列に自動変換されません

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), now it works

または、 `symbol.description` プロパティを取得して、説明のみを表示します。

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

「非表示」プロパティ

シンボルを使用すると、オブジェクトの「非表示」プロパティを作成できます。これは、コードの他の部分が誤ってアクセスしたり上書きしたりすることはできません。

たとえば、サードパーティのコードに属する `user` オブジェクトを操作しているとします。識別子を追加したいと思います。

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

let user = { // belongs to another code
  name: "John"
};

let id = Symbol("id");

user[id] = 1;

alert( user[id] ); // we can access the data using the symbol as the key

文字列 `"id"` の代わりに `Symbol("id")` を使用することの利点は何ですか?

`user` オブジェクトは別のコードベースに属しているため、フィールドを追加すると、その別のコードベースで定義済みの動作に影響を与える可能性があるため、安全ではありません。ただし、シンボルには誤ってアクセスすることはできません。サードパーティのコードは新しく定義されたシンボルを認識しないため、 `user` オブジェクトにシンボルを安全に追加できます。

また、別のスクリプトが独自の目的で `user` 内に独自の識別子を持ちたいと想像してください。

次に、そのスクリプトは次のように独自の `Symbol("id")` を作成できます。

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

user[id] = "Their id value";

シンボルは常に異なるため、名前が同じであっても、識別子の間に競合は発生しません。

…しかし、同じ目的でシンボルの代わりに文字列 `"id"` を使用した場合、競合が発生します。

let user = { name: "John" };

// Our script uses "id" property
user.id = "Our id value";

// ...Another script also wants "id" for its purposes...

user.id = "Their id value"
// Boom! overwritten by another script!

オブジェクトリテラルのシンボル

オブジェクトリテラル `{...}` でシンボルを使用する場合は、その周りに角かっこが必要です。

このように

let id = Symbol("id");

let user = {
  name: "John",
  [id]: 123 // not "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)

// the direct access by the symbol works
alert( "Direct: " + user[id] ); // Direct: 123

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` でレジストリに保存します。

例えば

// read from the global registry
let id = Symbol.for("id"); // if the symbol did not exist, it is created

// read it again (maybe from another part of the code)
let idAgain = Symbol.for("id");

// the same symbol
alert( id === idAgain ); // true

レジストリ内のシンボルは、グローバルシンボルと呼ばれます。コードのいたるところでアクセスできるアプリケーション全体のシンボルが必要な場合は、これがそのためのものです。

それはRubyのように聞こえます

Rubyなどの一部のプログラミング言語では、名前ごとに1つのシンボルがあります。

JavaScriptでは、見てきたように、それはグローバルシンボルに当てはまります。

Symbol.keyFor

グローバルシンボルの場合、 `Symbol.for(key)` は名前でシンボルを返すことがわかりました。反対のことを行うには、グローバルシンボルで名前を返すには、 `Symbol.keyFor(sym)` を使用できます。

例えば

// get symbol by name
let sym = Symbol.for("name");
let sym2 = Symbol.for("id");

// get name by 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, global symbol
alert( Symbol.keyFor(localSymbol) ); // undefined, not global

alert( localSymbol.description ); // name

システムシンボル

JavaScriptが内部で使用する多くの「システム」シンボルが存在し、それらを使用してオブジェクトのさまざまな側面を微調整できます。

仕様の既知のシンボルテーブルにリストされています。

  • Symbol.hasInstance
  • Symbol.isConcatSpreadable
  • Symbol.iterator
  • Symbol.toPrimitive
  • …など。

たとえば、 `Symbol.toPrimitive` を使用すると、オブジェクトからプリミティブへの変換を記述できます。その使用法はすぐにわかります。

対応する言語機能を学ぶと、他のシンボルにも精通するようになります。

まとめ

`Symbol` は、一意の識別子のためのプリミティブ型です。

シンボルは、オプションの説明(名前)を指定した `Symbol()` 呼び出しで作成されます。

シンボルは、名前が同じであっても、常に異なる値です。同じ名前のシンボルを等しくしたい場合は、グローバルレジストリを使用する必要があります。 `Symbol.for(key)` は、 `key` を名前としてグローバルシンボルを返します(必要な場合は作成します)。同じ `key` で `Symbol.for` を複数回呼び出すと、まったく同じシンボルが返されます。

シンボルには、主に2つのユースケースがあります。

  1. 「非表示」オブジェクトプロパティ。

    別のスクリプトまたはライブラリに「属する」オブジェクトにプロパティを追加する場合は、シンボルを作成してプロパティキーとして使用できます。シンボリックプロパティは `for..in` に表示されないため、他のプロパティと一緒に誤って処理されることはありません。また、別のスクリプトにはシンボルがないため、直接アクセスされることもありません。そのため、プロパティは誤用や上書きから保護されます。

    そのため、シンボリックプロパティを使用して、必要なものをオブジェクトに「密かに」隠すことができますが、他の人は見ることができません。

  2. JavaScriptで使用される多くのシステムシンボルがあり、 `Symbol.*` としてアクセスできます。それらを使用して、いくつかの組み込みの動作を変更できます。たとえば、チュートリアルの後半では、反復可能オブジェクトに `Symbol.iterator` を使用し、オブジェクトからプリミティブへの変換を設定するために `Symbol.toPrimitive` を使用します。

技術的には、シンボルは100%隠されていません。すべてのシンボルを取得できる組み込みメソッドObject.getOwnPropertySymbols(obj)があります。また、シンボルを含むオブジェクトのすべてのキーを返すReflect.ownKeys(obj)というメソッドもあります。ただし、ほとんどのライブラリ、組み込み関数、および構文構造はこれらのメソッドを使用しません。

チュートリアルマップ

コメント

コメントする前に読んでください…
  • 改善の提案がある場合は、コメントする代わりに、GitHub issue を送信するか、プルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • 短いコードを挿入するには <code> タグを使用し、複数行のコードには <pre> タグで囲み、10行を超える場合はサンドボックス(plnkrjsbincodepenなど)を使用してください。