30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

ES2015で追加されたプリミティブ型、シンボル

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

だが通常のキーと異なり、列挙されない。
プロパティの内部属性であるenumerabletrueなので通常は列挙されるはずだが、シンボルをキーにしたプロパティは列挙されない仕様らしい。

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によってJavaScriptECMAScript)は大幅にパワーアップし、今後も強化されていくはずだけど、そうなると問題になるのが後方互換性。
単に新しい名前のメソッドを導入してしまうと、既存のプログラムとの名前の衝突が起きてしまう。
シンボルを使えば、そういった事態を避けつつ、新しい機能を盛り込むことが出来る。

一般の開発者にとっては、シンボルを利用して導入された機能を理解し使いこなすことが重要であり、自分でシンボルを使っていくことはあまりないのかもしれない。
それぞれがユニークな値になるという特性があるので、プロトタイプ拡張などに使えるようではあるけれど。

参考資料