JavaScriptのプリミティブ型は文字列、数値、真偽値、undefined、nullの5つだったが、ES2015からシンボルが加わった。
Symbol()
を使うことで作成できる。引数は必須ではない。
const mySymbol = Symbol(); const mySymbol2 = Symbol('name'); const mySymbol3 = Symbol(1); const mySymbol4 = Symbol(null); const mySymbol5 = Symbol({prop:mySymbol}); console.log(mySymbol); // Symbol() console.log(mySymbol2); // Symbol(name) console.log(mySymbol3); // Symbol(1) console.log(mySymbol4); // Symbol(null) console.log(mySymbol5); // Symbol([object Object])
シンボルは全てユニークな存在で、同じ引数を渡しても別の存在として扱われる。オブジェクトと同様。
const mySymbol = Symbol(1); const mySymbol2 = Symbol(1); const obj = {key:'value'}; const obj2 = {key:'value'}; console.log(mySymbol === mySymbol2); // false console.log(obj === obj2); // false
for
シンボルを共有したい場合は、Symbol.for()
を使ってシンボルを作成する。
そうすると、同じ引数を使った場合は必ず同一のシンボルが返ってくる。
また、Symbol.for()
で作成したシンボルに対してSymbol.keyFor()
を使うと、そのシンボルがどの引数に紐付けられているかを確認できる。
const mySymbol = Symbol.for(1); const mySymbol2 = Symbol(1); const mySymbol3 = Symbol.for(1); console.log(mySymbol === mySymbol2); // false console.log(mySymbol === mySymbol3); // true console.log(mySymbol2 === mySymbol3); // false console.log(Symbol.keyFor(mySymbol)); // 1 console.log(Symbol.keyFor(mySymbol2)); // undefined
型変換
シンボルを数値に変換することは出来ない。
文字列には変換できるが、暗黙の変換は行われない。
真偽値への変換は、true
になる。
const mySymbol = Symbol(1); // TypeError: Cannot convert a Symbol value to a number // console.log(Number(mySymbol)); console.log(mySymbol.toString()+' <-string'); // Symbol(1) <-string // TypeError: Cannot convert a Symbol value to a string // console.log(mySymbol+' <-string'); let result = 'no execute'; if(mySymbol){ result = 'execute'; }; console.log(result); // execute result = 'no execute'; if(Symbol(null)){ result = 'execute'; }; console.log(result); // execute
ラッパーオブジェクトとプロトタイプチェーン
文字列や数値型と同じように、シンボルをオブジェクトのように扱う(プロパティやメソッドにアクセスしようとする)と、自動的にラッパーオブジェクトが発生する。
また、これも文字列や数値型と同じだが、new Object()
の引数にシンボルを渡すと、明示的にラッパーオブジェクトを作ることが出来る。
const symbolWrapper = new Object(Symbol(1)); console.log(symbolWrapper); // [Symbol: Symbol(1)] console.log(Symbol.prototype.isPrototypeOf(symbolWrapper)); // true console.log(Object.getOwnPropertyNames(Symbol.prototype)); // [ 'constructor', 'toString', 'valueOf' ] console.log(Object.prototype.isPrototypeOf(Symbol.prototype)); // true
プロトタイプチェーンも他のデータ型と同じで、シンボルのラッパーオブジェクトはSymbol.prototype
を参照し、さらにそこからObject.prototype
へと辿っていく。
プロパティのキーとしてのシンボル
文字列と同じ形で、オブジェクトのプロパティのキーとして使える。
const mySymbol = Symbol(); let obj = {}; obj[mySymbol] = 'value'; console.log(obj[mySymbol]); // value
だが通常のキーと異なり、列挙されない。
プロパティの内部属性であるenumerable
がtrue
なので通常は列挙されるはずだが、シンボルをキーにしたプロパティは列挙されない仕様らしい。
const mySymbol = Symbol('name'); const myString = 'moji'; const obj = {}; obj[mySymbol] = 'value'; obj[myString] = 'value2'; // 存在は確認できるが…… console.log(obj.hasOwnProperty(mySymbol)); // true console.log(mySymbol in obj); // true // 取得は出来ない console.log(Object.getOwnPropertyNames(obj)); // [ 'moji' ] for(let key in obj){ console.log(key); // moji }; // enumerableはtrueだが、無視されるらしい console.log(Object.getOwnPropertyDescriptor(obj, mySymbol)); // { value: 'value', // writable: true, // enumerable: true, // configurable: true }
そのため、スコープの外に出るなどしてシンボルへの参照が失われてしまったら、既存の方法では二度とそのプロパティにアクセスできない。
let obj = {}; (()=>{ const mySymbol = Symbol(); const string = 'moji'; obj[mySymbol] = 'value'; obj[string] = 'value2'; console.log(obj[mySymbol]); // value console.log(obj[string]); // value2 })(); // スコープの外に出たので、mySymbolとstringはundefinedになる // value2には、obj.mojiでアクセスできる console.log(Object.getOwnPropertyNames(obj)); // [ 'moji' ] console.log(obj.moji); // value2 // 無名関数で実行したSymbol()とここで実行したSymbol()は別物なので、undefinedになる console.log(obj[Symbol()]); // undefined
この状況を回避する方法は、いくつかある。
Symbol.for()の利用
前述のSymbol.for()
を使う。
let obj = {}; (()=>{ const mySymbol = Symbol.for(); obj[mySymbol] = 'value'; console.log(obj[mySymbol]); // value })(); console.log(Object.getOwnPropertyNames(obj)); // [] // 無名関数で実行したSymbol.for()とここで実行したSymbol.for()は、同一のシンボルを返す // そのため、プロパティにアクセスできる console.log(obj[Symbol.for()]); // value
Object.getOwnPropertySymbols()
Object.getOwnPropertySymbols()
は、引数として渡したオブジェクトのプロパティのキーに使われているシンボルを列挙するメソッド。
これを使うことで、キーとして使われているシンボルを参照できる。
let obj = {}; (()=>{ const mySymbol = Symbol(); obj[mySymbol] = 'value'; console.log(obj[mySymbol]); // value console.log(Object.getOwnPropertyNames(obj)); // [] })(); console.log(Object.getOwnPropertyNames(obj)); // [] console.log(Object.getOwnPropertySymbols(obj)); // [ Symbol() ] const key = Object.getOwnPropertySymbols(obj)[0]; console.log(obj[key]); // value console.log(obj.hasOwnProperty(key)); // true
シンボルの利用
シンボルというデータ型が追加されたのは仕様策定の都合によるもの、らしい。
ES2015によってJavaScript(ECMAScript)は大幅にパワーアップし、今後も強化されていくはずだけど、そうなると問題になるのが後方互換性。
単に新しい名前のメソッドを導入してしまうと、既存のプログラムとの名前の衝突が起きてしまう。
シンボルを使えば、そういった事態を避けつつ、新しい機能を盛り込むことが出来る。
一般の開発者にとっては、シンボルを利用して導入された機能を理解し使いこなすことが重要であり、自分でシンボルを使っていくことはあまりないのかもしれない。
それぞれがユニークな値になるという特性があるので、プロトタイプ拡張などに使えるようではあるけれど。