Symbolとは
- ECMAScript 6で追加されたプリミティブデータ型。
- Symbol() 関数を呼び出すことで生成され、動的に無名の一意の値を生み出す。
- well-known symbols と呼ばれる定数がある。
- グローバルシンボルがある。
well-known symbols と呼ばれる定数がある。
MDN Web Docsでたまに見かける@@始まりで表現されている「@@iterator 」等のことです。
※ECMAScriptでwell-known symbolsをいちいちSymbole.iteratorと書かなくても@@iteratorと書いてもいいよ、ってなっています。
well-known symbolsの詳細については、実際well-known symbolsを使用する「イテレーターとジェネレーター」などで説明します。ので、ここでは割愛します。
Symbol() 関数を呼び出すことで生成され、動的に無名の一意の値を生み出す。
Symbolなんて使ったこと無い。という方もいる(私も)いますが、こんな使い方があるんだー。と感じてもらい、よければ実践でも使ってもらえればと思います。
一意の値を生み出す。
一意の値って??
先輩にプログラムコーディングではリレラル値の使用はしないで、定数を使え!って教わった方もいると思います。
こんな感じで
// 血液型
const BLOOD_TYPE_A = 1;
const BLOOD_TYPE_B = 2;
const BLOOD_TYPE_O = 3;
let aokiBloodType = BLOOD_TYPE_A;
if (aokiBloodType === BLOOD_TYPE_A) {
console.log('ほんまにA型か?');
}
さらに情報を増やすためにあたらに「性別」の定数を追加します。
// 血液型
const BLOOD_TYPE_A = 1;
const BLOOD_TYPE_B = 2;
const BLOOD_TYPE_O = 3;
// 性別
const GENDER_MALE = 1;
const GENDER_FEMALE = 2;
let aoki = {
gender: GENDER_MALE,
BloodType: BLOOD_TYPE_A,
}
if (aoki.BloodType === BLOOD_TYPE_A) {
console.log('ほんまにA型か?');
}
if (aoki.gender === GENDER_MALE) {
console.log('ほんまに男か?');
}
ほんまにA型か?
ほんまに男か?
ま、特に問題なさそうなんですが、たまにTYPOしてしまうこともあるでしょう。
// 血液型
const BLOOD_TYPE_A = 1;
const BLOOD_TYPE_B = 2;
const BLOOD_TYPE_O = 3;
// 性別
const GENDER_MALE = 1;
const GENDER_FEMALE = 2;
let aoki = {
gender: GENDER_MALE,
BloodType: BLOOD_TYPE_A,
}
if (aoki.BloodType === BLOOD_TYPE_A) {
console.log('ほんまにA型か?');
}
if (aoki.gender === BLOOD_TYPE_A) { // <--TYPO、間違って BLOOD_TYPE_A を指定
console.log('ほんまに男か?');
}
TYPOしていても、実行結果が同じになりました!。当然ですが。。
ほんまにA型か?
ほんまに男か?
これは、潜在的なバグになります。今は「たまたま」期待通りの結果になりましたが、
もし、仕様変更で「BLOOD_TYPE_A=10」になったら、期待する結果にならなくなってしまいます。
このような潜在的なバグを生む原因の一つとして、定数の値が一意の値でないからです。
もし、定数の値が一意であれば、動作検証の時に気づくはずです。
TypeScriptでは、このような列挙値は型チェックで防げているんだろうなー。
で、完全に一意にしたいなら、Symboleを使ってもいいじゃないか?って話になります。
// 血液型
const BLOOD_TYPE_A = Symbol();
const BLOOD_TYPE_B = Symbol();
const BLOOD_TYPE_O = Symbol();
// 性別
const GENDER_MALE = Symbol();
const GENDER_FEMALE = Symbol();
let aoki = {
gender: GENDER_MALE,
BloodType: BLOOD_TYPE_A,
}
if (aoki.BloodType === BLOOD_TYPE_A) {
console.log('ほんまにA型か?');
}
if (aoki.gender === GENDER_MALE) {
console.log('ほんまに男か?');
}
if (aoki.gender === BLOOD_TYPE_A) { // <--TYPO、間違って BLOOD_TYPE_A を指定
console.log('TYPO: ほんまに男か?');
}
ほんまにA型か?
ほんまに男か?
TYPOのif条件も正常に判断されました。
一意の値ということは、この値を使ってプロパティを定義をすれば、他の人が定義したプロパティとかぶることは無く安心じゃないか??
もう一つの使い方として、プロパティ名として、Symbolを使うことも出来ます。
沢山の人でアプリを作っていると、
エンハンスなどを繰り返しているうちに、しらない間に既存のプロパティの値を上書きしてしまう!!
ようなこと。があるかも。。。。。
// Aさん: オブジェクト作成
let person = {
name: 'AOKI',
age: 18,
delFlg: 0, // 削除フラグ
}
// Bさん: 別の意味で削除を追加!!!!
// しかし既にあるので、上書きしてしまう!!!!
person['delFlg'] = 'A';
// Aさん: なんかの処理でdelFlg参照
// 思わぬ値で上書きされ、バッグってしまう。
if (person.delFlg === 0) {
...
}
このようなことを避ける為に
// 削除フラグプロパティ名
const propDelFlg = Symbol('delFlg');
let person = {
name: 'AOKI',
age: 18,
[propDelFlg]: 0, // 削除フラグ
}
と、プロパティ名をSymbolで定義しておけば、プロパティ名が被ることが無くなります。
また、Symbolで定義したプロパティはイテレーターでは対象外になるので、隠しプロパティとして扱えます!!
※Object.getOwnPropertySymbols()を使えばSymbol定義したプロパティを取得できますが、そこまでして取得する人はいないでしょう。。
// 削除フラグプロパティ名
const propDelFlg = Symbol('delFlg');
let person = {
name: 'AOKI',
age: 18,
[propDelFlg]: 0, // 削除フラグ
}
// プロパティ名を検索
for (let key in person) {
console.log(key);
}
name
age
Symbolをプロパティ名として使っても安全か??
クローンしたりしても、消えないよね??
検証しました。
// 削除フラグプロパティ名
const propDelFlg = Symbol('delFlg');
const person = {
name: 'AOKI',
age: 18,
[propDelFlg]: 0, // 削除フラグ
}
// 複製してみる。
const cloned = Object.assign( {}, person);
console.log(`cloned: ${cloned[propDelFlg]}`);
// シリアライズ、デシリアライズしてみる
const serialized = JSON.stringify(person);
const deserialized = JSON.parse(serialized);
console.log(`deserialized: ${deserialized[propDelFlg]}`);
cloned: 0
deserialized: undefined
クローンは問題なし!!
さすがにシリアライズしたら消えたので、ご注意を!!