シンボルとは
とあるサンプルコードを見ていたら array[Symbol.iterator]()
なんていう記述を見かけました。普段見慣れないコードだったので調べてみると、どうやら「シンボル」というやつらしいです。ES2015から登場したものみたいですが、あまり馴染みがなかったので少し調べてみました。
シンボルはES2015から新たに導入されたプリミティブデータ型の1つです。プリミティブデータ型とはオブジェクトではないデータ型のことで、文字列、数値、真偽値、null、undefined、そしてシンボルがあります。(JavaScriptでは、関数や配列はオブジェクトの一種です。)
シンボルはSymbol
関数によってのみ作成することができます。シンボルの実体は「唯一無二の何か」であり、決して競合しないユニークなキーのようなものですが文字列データではありません。そのため、console.logで中身を見ようとしても文字表現ができないため、下記のような出力になります。
const sym = Symbol(); console.log(sym); //[object Symbol] { ... } console.log(typeof sym); //symbol
一旦作ったシンボルは、それ自身とのみ等しくなります。同じものを作ることは出来ません。そういう意味では空のオブジェクトに近いかもしれません。
const sym1 = Symbol(); const sym2 = Symbol(); console.log(sym1 === sym2); //false
シンボルを生成する際にはnew演算子は不要です(newを使うとエラーになります)。また、Symbol()関数はパラメータを1つ指定することができますが、このパラメータはデバッグ用途なので生成されるシンボル自体には影響を与えません。
const sym1 = Symbol(); const sym2 = new Symbol(); //TypeError const sym3 = Symbol('hoge'); console.log(sym3.toString()); // Symbol(hoge)
シンボルはオブジェクトのプロパティのキーとして使用することができます。このことが重要で、シンボルの存在意義と言っても過言ではないかと思います。
const sym = Symbol(); const obj = {}; obj[sym] = 'hoge'; console.log(obj[sym]); //hoge
シンボルをプロパティのキーとして使用する際、なんとなく次の様な書き方をしてしまいそうですが、これだと単純にオブジェクトに”sym”という名前のプロパティを追加しているだけなので、これだと意味がありません。
const sym = Symbol(); const obj = { sym: 'fuga' }; console.log(obj[sym]); //undefined
上記コード例の「sym」はあくまで変数です。プロパティのキーを変数で表現したい場合は、次の例の様にブラケットを使った表記でなくてはなりません。
const sym = Symbol(); const obj = { [sym] = 'fuga' }; // または // obj[sym] = 'fuga';
シンボルが必要な理由
シンボルは唯一無二の値であり、プロパティのキーとして使用することができるという特性上、名前の衝突を回避することに活用することができます。しかし、名前の衝突ならグローバル変数を使わないようにしたり、名前空間を用いればどうとでも回避できることです。ならばなぜわざわざシンボルなどというものが出来たのでしょうか?
一番の理由は、JavaScriptの互換性を確保するためのようです。これについては下記のページでとても分かりやすく書いてありますので一読をお勧めします。
ECMAScript6にシンボルができた理由 – Qiita
シンボルは私たちのようなJavaScriptを使ってコードを書く側の人間ではなく、JavaScriptの仕様策定側にとって最も必要な仕組みだった、というわけですね。
世界中に存在するJavaScriptで書かれた既存プログラムを壊すことなく、安全にJSをアップデートするためにはシンボルのような仕組みが必要だったということです。
今後JSのアップデートに伴い、シンボルを利用した拡張なども行われてくると考えられます。ES2015で追加された機能のいくつかはすでにこの仕組みに依存しています。
シンボルの活用
シンボルはJSの互換性を確保するための重要な仕組みではありますが、その性質を利用して色々なことに応用できそうです。といいつつも、わざわざシンボルを活用しなければならないケースというのも実際のところあまり思いつきません。
ネット上ではいくつかの活用例が紹介されていますが、その中でも最も有用であろう活用例を次に紹介します。
ビルトインオブジェクトの拡張(オレオレメソッド)
JavaScriptでは、Object、Array、Math、Dateのようなオブジェクトは最初からランタイムに組み込まれているもので、これらをビルトインオブジェクトといいます。
参考:標準ビルトインオブジェクト
ビルトインオブジェクトはグローバルオブジェクトなので、どの文脈からでも使用できます。また、プログラマーがビルトインオブジェクトにメソッドを追加したり、既存のメソッドを上書きしたりと拡張することも可能です。
ですが、ES6以前のJavaScriptでは、ビルトインオブジェクトの拡張はアンチパターンとされてきました。例えば、同一ページで複数のライブラリを読み込んだ場合、それぞれのライブラリ内で同一のビルトインオブジェクトに同じ名前のメソッドを追加していたりした場合、名前の競合が発生し、プログラムが正常に動作しなくなるリスクがあるからです。
ところが、ES6で登場したシンボルを活用することで、名前の競合リスクをなくし、安全にビルトインオブジェクトを拡張させることができるようになるため、ビルトインオブジェクトの拡張がアンチパターンであるとは一概には言い切れなくなります。
次の例では、A.js、B.jsという2つのモジュールがあります。それぞれが全く別の開発者によって書かれたコードであり、各々がJavaScriptの標準ビルトインオブジェクトである Object に対し、独自のメソッドを追加しています。
(※コードの実用性は全くありませんが、サンプルなのでご容赦ください)
■名前の競合が発生する例
/** ビルトインオブジェクトである Object を拡張して、 自身の name プロパティを返すメソッドを追加 */ Object.prototype.getName = function () { return this.name; };
/** ビルトインオブジェクトである Object を拡張して、 hanako という名前を返すメソッドを追加 */ Object.prototype.getName = function () { return 'hanako'; };
import './A.js'; import './B.js'; console.log({name: 'taro'}.getName()); // hanako
<html> <head> <script type="module" src="./index.js"> </head> <body> </body> </html>
index.jsではこれら2つのモジュールをA.js→B.jsの順で読み込んでいます。
A.jsの開発者からすると、B.jsの内部で何が起こっているのかが分かりません。当然、Object.prototype.getName では新たに作成したオブジェクトの name プロパティ(ここではtaro)を返すことを期待しています。しかし、B.jsの方で同名のメソッドが定義されており、かつB.jsの方が後に読み込まれているため期待した結果を得ることができません。
モジュール設計者からすると、ビルトインオブジェクトを拡張する場合、名前の一意性が保証されないことが問題です。しかし、この問題は次のようにしてシンボルを活用することで解決することができます。
■シンボルを使い名前の競合を回避する例
const sym = Symbol(); //作成したシンボルをキーにしたメソッドを定義する Object.prototype[sym] = function () { return this.name; }; //シンボルをモジュールの外から参照できるようにエクスポートする export {sym as default};
const sym = Symbol(); //作成したシンボルをキーにしたメソッドを定義する Object.prototype[sym] = function () { return 'hanako'; }; //シンボルをモジュールの外から参照できるようにエクスポートする export {sym as default};
import symA from './A.js'; import symB from './B.js'; //変数 symA と symB の値は決して競合しないので、 //安全に目的のメソッドを実行できる console.log({name: 'taro'}[symA]());
上記例では、ES2015のモジュールの仕組み(import/export)を利用したサンプルを紹介しましたが、もちろんSymbolの使い方はこの限りではありません。
要は、シンボルをメソッド名にすれば安全にビルトインオブジェクトを拡張することができ、また、ビルトインオブジェクトの拡張に使用したシンボルを外部から参照できるようにすれば問題無く目的のメソッドを実行することができるということです。
ビルトインオブジェクトに限らず名前の干渉が起こりうるようなところでは、シンボルを利用してうまく問題を処理できるケースもあるかと思います。
ウェルノウンシンボル
ウェルノウンシンボルとは、JavaScriptにであらかじめ定義されているビルトインシンボルのことで、言語内部の振る舞いを表現するためのシンボルです。これらのシンボルをメソッドとして定義し、そのメソッドを適切に実装することによりJSランタイム側から参照させるようにすることができます。
ビルトインシンボルをうまく活用することにより、よりスマートに機能実装ができる可能性があります。
ウェルノウンシンボルには次のようなものがあります。
参考:ウェルノウンシンボル
ここではウェルノウンシンボル活用の一例として、新しく作成したオブジェクトをスプレッド演算子で直接展開できるようにしてみます。
const items = {}; //Symbol.iteratorを実装していないので、当然エラーになる console.log(...items); //TypeError: undefined is not a function
const items = { [Symbol.iterator]() { return { index: 0, next() { return this.index < 3 ? {value: this.index++} : {done: true}; } }; } }; console.log(...items); // 0 1 2
ウェルノウンシンボルを実装する場合、シンボル毎に実装の作法が異なります。 シンボルを自前で定義するよりも、むしろこちらの方が使用頻度が多いかもしれません。
注意点
シンボルはテキスト表現が出来ないデータ型のためJSONとは相性が悪く、JSONに変換しようとしてもできません。下記の例の様にオブジェクトのキー、値いずれの場合でもJSON.stringify()
では完全に無視されてしまいます。
const sym = 'foo'; console.log(JSON.stringify(sym)); // undefined const obj = { foo: 'foo', [Symbol()]: 'bar', baz: Symbol() }; console.log(JSON.stringify(obj)); // {"foo": "foo"} const ary = [Symbol()]; console.log(JSON.stringify(ary));
また、for...in
ループやObject.keys()
を使っても、シンボルプロパティは列挙されず無視されますで注意が必要です。
const obj = { foo: 'foo', [Symbol()]: 'bar' }; let prop; for (prop in obj) { console.log(prop); // foo }; console.log(Object.keys(obj)); // ["foo"]
まとめ
シンボルは完全にユニークなキーであり、またプロパティにも使用可能であることから名前の競合という問題への対処に活用することができそうです。ですが、シンボルの一番の存在意義は「JavaScriptの互換性」でしょうし、名前の衝突の回避であれば他にも色々と方法があるのでわざわざシンボルを使う必要もさほどないと思います。(もちろん、選択肢の1つとしてはアリです)
本記事でサンプルとして紹介したビルトインオブジェクトの拡張についても、必要となるケースは稀だと思います。
個人的にはむしろウェルノウンシンボルを使ったテクニックの方が使いどころがありそうな気がします。
このように書くとシンボル自体使うことがあまりなさそうな気がしますが、今後、シンボルを利用したJavaScriptの拡張が増えていくと考えられるので、必然的に利用するケースは多くなってくるでしょう。ですので、シンボルの意味だけでも理解しておくことは大切だと思います。