投稿日:2020年12月11日
シンボル値は、オブジェクトプロパティの識別子として使用できます。これがデータ型の主な利用目的ですが、不透明なデータ型の有効化や、実装サポートされている一意の識別子として機能するなど、他の利用目的も存在します。この記事ではSymbolの利用方法について解説します。
// これは値渡し
let num1 = "abc";
let num2 = num1;
// どちらの変数も参照先が同じ
console.log(num1 == num2); // true
num2 = "a" + "b" + "c";
// 値が同じなら参照先は常に同じ
console.log(num1 == num2); // true
let obj1 = { name: "marsquai" };
let obj2 = obj1;
// どちらの変数も参照先が同じ
console.log(obj1 == obj2); // true
obj2 = { name: "marsquai" };
// 参照先が変わった
console.log(obj1 == obj2); // false
プリミティブ型とオブジェクトで挙動が違うことが確認できますね。
Symbolは常に一意の値を返すためオブジェクトと非常に似た動きをしますが、実際にはプリミティブ型です。
下に簡単な使い方を示します。
let symbol1 = Symbol();
let symbol2 = Symbol();
//毎回違う値になる
console.log(symbol1 == symbol2); // false
この様にSymbolはSymbol()というグローバルの静的メソッドを使い生成します。
値はその環境の中で常に一意となります。
Symbolの有益さについて説明する前に、JavaScriptのObjectの特徴について説明しておきます。
Symbolのプロパティとしての利用について解説します。
と、その前にJavaScriptのある性質について話しておく必要があります。
JavsScriptではObjectのプロパティが動的です。そのため以下のようなコードを書くことができます。
/**
* ユーザークラス
*/
class User {
name;
constructor(_name) {
this.name = _name;
}
}
/**
* 配列に連番を追加
* @param users {Array} 配列
*/
function addIdToUsers(users) {
users.forEach((element, i) => {
element.id = i + 1;
});
}
/**
* 配列を初期化
*/
const users = [
new User("mars quai"),
new User("Ichigo Kurosaki"),
new User("Rukia Kuchiki"),
];
addIdToUsers(users);
console.log(users);
[ User { name: 'mars quai', id: 1 },
User { name: 'Ichigo Kurosaki', id: 2 },
User { name: 'Rukia Kuchiki', id: 3 } ]
addIdToUsers()関数は渡された配列に連番のidをプロパティとして設定する関数です。Userクラスはnameというプロパティだけを持っているのですが、addIdToUsers()関数で後からidプロパティを追加しています。
この様にJavaScriptのObjectのプロパティは生成時から変化しうる動的な性質を持ちます。
これは非常に便利な機能なのですが、あるバグを生じる可能性があることがわかるでしょうか?
それは対象のObjectがすでに追加したいプロパティを持っていた場合です。
例えば上記Userクラスに対して、別のプログラマーが以下のようなクラスを作ってしまったとしましょう。
/**
* Userを継承した管理者クラス
*/
class SuperUser extends User {
id;
password;
constructor(_name, _id, _password) {
super(_name);
this.id = _id;
this.password = _password;
}
}
SuperUserクラスはすでにidをいうプロパティを持っています。このインスタンスの配列でうっかりaddIdToUsers()を呼び出してしまうと、idというプロパティが上書きされてしまいます!
/**
* 配列を初期化
*/
const users = [
new SuperUser("mars quai", "marsquai", "marsquai0203"),
new SuperUser("Ichigo Kurosaki", "kurosaki", "kurosaki0900"),
];
addIdToUsers(users);
console.log(users);
[ SuperUser { name: 'mars quai', id: 1, password: 'marsquai0203' },
SuperUser { name: 'Ichigo Kurosaki', id: 2, password: 'kurosaki0900' } ]
インスタンス生成時に渡したidが意図せず上書きされてしまいました。
JavaScriptではプロパティが動的であるが故に、プロパティの衝突の可能性をはらんでいます。
ではここでSymbolを使う例を考えてみましょう。
Symbol()は毎回一意な値を返す為、衝突を起こさないプロパティのキーとして扱うことができます。addIdToUsersProperty()関数を以下の様に変更してみます。
[...]
/**
* 配列に連番を追加
* @param users {Array} 配列
*/
const addIdToUsersProperty = Symbol();
function addIdToUsers(users) {
users.forEach((element, i) => {
element[addIdToUsersProperty] = i + 1;
});
}
[...]
Symbol()関数でaddIdToUsers()で使用するプロパティのキーを生成しています。このように定義することでプロパティの衝突は絶対に起こりません。
/**
* 配列を初期化
*/
const users = [
new SuperUser("mars quai", "marsquai", "marsquai0203"),
new SuperUser("Ichigo Kurosaki", "kurosaki", "kurosaki0900"),
];
addIdToUsers(users);
console.log(users);
[ SuperUser {
name: 'mars quai',
id: 'marsquai',
password: 'marsquai0203',
[Symbol()]: 1 },
SuperUser {
name: 'Ichigo Kurosaki',
id: 'kurosaki',
password: 'kurosaki0900',
[Symbol()]: 2 } ]
一つ注意する点として、SymbolはJSON.stringfy()やObject.keys()では吐き出されません。しかしプロパティは確かに存在します。
/**
* 配列を初期化
*/
const users = [
new SuperUser("mars quai", "marsquai", "marsquai0203"),
new SuperUser("Ichigo Kurosaki", "kurosaki", "kurosaki0900"),
];
addIdToUsers(users);
console.log(Object.keys(users));
console.log(JSON.stringify(users));
console.log(users[0][addIdToUsersProperty]);
[ '0', '1' ]
[{"name":"mars quai","id":"marsquai","password":"marsquai0203"},{"name":"Ichigo Kurosaki","id":"kurosaki","password":"kurosaki0900"}]
1
Symbol()はあくまでそのプログラムの中でだけ利用するプロパティで使用するのがよさそうですね。