【JavaScript】 シンボル(Symbol)とは?使い方を解説します
更新日:2023/07/20
JavaScriptにはシンボルという機能があります。
あまりメジャーな機能ではないので知らないという人が多いようです。
そこで今回はシンボルについて調べてみました。
シンボルとは
シンボル(Symbol)は、他のシンボルと値が重複しない値をあらわします。
他と重複しない値を作成したいとき、
シンボルは重複しない
const sym=[];
for( let i = 0 ; i < 100 ; i ++){
sym.push( Symbol() );
}
上の例の Symbol() は、新しいシンボル値を生成する関数です。
生成された値はSymbolオブジェクトのインスタンスではなくて、Symbolプリミティブです。
Symbol() ← Symbolプリミティブを生成
SymbolオブジェクトはSymbolを操作するための関数などが実装されています。
一方Symbolプリミティブは、数値プリミティブや文字プリミティブと同じように、値そのものです。
プログラムコード上でSymbolプリミティブに対して、Symbolオブジェクトのプロパティを参照させようとすると、自動的にプリミティブがオブジェクトに変換されます。
コード上で使用する際は意識しなくても特に問題ありませんが、一応覚えていた方がいいでしょう。
Symbolは値そのものをデータとして使用できない
上の例ではSymbolプリミティブを100回生成して、配列symに順番に格納しています。
格納した値は、同じ値が仕様上は存在しません。
ここで"仕様上は"と書いたのは、プログラムコード上ではシンボルの値を確認できないからです。
例えば次のように生成したシンボル値を、コンソールに表示してみます。
const sym1 = Symbol();
console.log( sym1 ); // 結果: "Symbol()"
const sym2 = Symbol();
console.log( sym2 ); // 結果: "Symbol()"
Symbolプリミティブが実際にどのような値を持っているのかわかりませんが、ブラウザやデバッガーに表示するには一度文字列に変換する必要があります。
しかしSymbolプリミティブはただの値なので、文字列を生成して返す機能を持っていません。
実際には表示する文字列を生成するために、Symbolオブジェクトのインスタンスに変換(ラップ)されます。
Symbolプリミティブ → Symbolオブジェクトのインスタンスに変換
その後インスタンスのtoString ( )が呼ばれるのですが、toString ( )は"Symbol()"という文字列を生成するだけです。値をそのまま出してくれないのです。
そのため上の結果は、異なるシンボル値でも同じ"Symbol()"という文字列が表示されてます。
シンボルは実際の値を表示する手段を提供していないのです。
このことから、例えば重複しないことが前提のIDなどの値に使用することはできません。
次のような使い方はできない
const syain = {
syain_ID = Symbol(),
syain_Name = "Taro",
}
なぜなら、syain_IDの値を見ようとすると"Symbol()"になってしまうからです。
Symbolは値そのものをデータとして使用してはいけないのです。
なお資料によっては、toString()を使用せずに文字列変換をおこなうと"Cannot convert a Symbol value to a string"などのエラーが発生すると書いてあるものもあります。
この点については未確認ですが、ECMAScriptの以前のバージョンや実装による違いなどで結果が異なる可能性があります。
上例のような結果を取得したいときは、次のようにtoString()を呼び出すことをお勧めします。
const sym1 = Symbol();
console.log( sym1.toString() ); // 結果: "Symbol()"
シンボルの比較
シンボル値を文字列に変換して値を確認することはできませんが、同一の値を参照しているかどうかの比較はできます。
const sym1 = Symbol();
const sym2 = sym1;
const sym3 = Symbol();
console.log( sym1 === sym2 ); // 結果: true
console.log( sym1 === sym3 ); // 結果: false
最初にシンボルは他のシンボルと重複しないと書きました。
しかし上の例では、sym1とsym2が同じ値になっています。
別の記事でも書きましたが、変数は実データの位置を示すラベル的なものを格納しています。
シンボルデータそのものは重複していないのです。
Symbol()関数の引数
なおシンボル作成時に、引数として文字列を指定できます。
const sym = Symbol("シンボル1");
この文字列はシンボル値の生成に影響を与えません。
const sym1 = Symbol("シンボル1");
const sym2 = Symbol("シンボル1");
上の例のように、同じ文字列でシンボル値を作成したとしても、重複はしません。
唯一影響があるのが、文字列に変換されたときです。
const sym1 = Symbol("シンボル1");
console.log( sym1 ); // 結果: Symbol(シンボル1)
Symbol(引数文字列) という形式に変換されます。
しかしあまり意味がない機能です。
引数で与えた文字列を知りたいときは、descriptionプロパティを参照します。
const sym1 = Symbol("シンボル1");
console.log( sym1.description ); // 結果: シンボル1
シンボルの使い方
シンボルはどのような方法で使用するのでしょうか。
プロパティキーとして使用
シンボルは、オブジェクトのプロパティキーとして使用します。
その際、[ ]でアクセスします。
×オブジェクト.シンボル値 〇オブジェクト[シンボル値]
各々別のプロパティとして扱われるので注意が必要です。
const name = Symbol();
const log = Symbol();
const obj = {
name : "太郎", // nameという名前のプロパティ
[name] : "花子", // シンボル値nameのプロパティ
log : function(){ console.log( this.name ) },
[log] : function(){ console.log( this[name] ) }
}
console.log( obj.name ); // 結果: "太郎"
console.log( obj[name] ); // 結果: "花子"
「俺ってシンボルをちゃんと理解して使ってる!」と積極的に使っていきたくなる例ですね。
しかしどちらのプロパティを扱いたいのかわかりにくい、第三者がコードを追うとき非常に困ります。
obj.nameはobj["name"]という表記もできます。シンボルの書き方ととても似ています。これでは、コード作成者がどちらの値を使用するつもりなのか判断が難しいですね。
バグの元になるので、このような同じ名前の使用は避けるべきですね。
ちなみにシンボルで定義したメソッドは、次のように ( ) をつけることで実行できます。
obj[log](); // 結果: "花子"
なんだか不思議な書き方ですが、通常のプロパティも同じように書くことができます。
obj.log(); // 結果: "花子"
obj["log"](); // 結果: "花子"
二つ目の書き方はシンボルと同じですね。
シンボルの使用目的
シンボルの使用目的はECMAScriptに書かれていないので類推するしかありません。
シンボルの特性上、「プロパティ名の重複を避ける」ことと、「外部からアクセスできないプロパティを作成する」こと、「定数としての利用」の3点が考えられます。
プロパティ名の重複を避ける
JavaScriptに元から定義されている標準オブジェクトに、独自のメソッドを追加すると便利になることがあります。
例として次のような、文字列を逆転する関数を作成しました。
文字列を逆転する関数
const stringReverse = t => [...t].map( (e,i,arr)=>arr[arr.length-i-1] ).join("");
console.log( stringReverse("僕は😺が好き") ); // 結果: き好が😺は僕
引数として受け取った文字列tを一文字ごとの配列に変換し、mapメソッドで文字を逆に並べた新しい配列を作成し、joinで結合しています。
[...t]は何のためにやってるの? という方は次のページを見てください。
■【JavaScript】 コード中の「...」は意味があった スプレッド/レスト構文
■【JavaScript】 文字列データの内部形式と関連メソッドについてまとめてみた
このコードは、引数が文字列かどうか確認していないなどの問題があります。
これを次のようにStringオブジェクトに組み込んでしまえば、問題も解決して便利になります。
文字列を逆転するStringメソッド
String.prototype.reverse = function (){
return [...this].map( (e,i,arr)=>arr[arr.length-i-1] ).join("");
};
console.log( "僕は😺が好き".reverse() ); // 結果: き好が😺は僕
console.log( "僕は😺が好き".reverse().reverse() ); // 逆転して逆転する 結果: 僕は😺が好き
基本的に僕はアロー関数が好きなのですが、アロー関数はthisを受け取らないのでfunction関数も使用しています。
call()やapply()などを使わなければ、thisがStringに固定されるので型チェックが不要です。
さらにメソッドチェーンできるので、コードの見た目が良くなります。
個人的には積極的に使っていきたい手法なのですが、標準オブジェクトはグローバルのため外部のJavaScriptコードにも影響を与えてしまいます。「他の人が同じ名前を使って、メソッドを上書きしている可能性があるよね」ということです。
この問題はシンボルを使用することで解決できます。
文字列を逆転するStringメソッドSymbol版
const reverseSymbol = Symbol();
String.prototype[reverseSymbol] = function (){
return [...this].map( (e,i,arr)=>arr[arr.length-i-1] ).join("");
};
console.log( "僕は😺が好き"[reverseSymbol]() ); // 結果: き好が😺は僕
console.log( "僕は😺が好き"[reverseSymbol]()[reverseSymbol]() ); // 結果: 僕は😺が好き
reverseSymbolを外部で使用できるようにしていない限り、このメソッドは外部で使用できませんし上書きもできません。
ただし僕的には、"僕は😺が好き"[reverseSymbol]()[reverseSymbol]()という複雑怪奇というか未知との遭遇的なコードになるので、気分的に使いたくないですね。
外部からアクセスできないプロパティを作成する
使用目的の二つ目は、外部からアクセスできないプロパティを作成することです。
この場合の外部とは、オブジェクトの外側です。
他の言語でprivateやprotectedなどで修飾されるプロパティのようなものを想定しています。
const name = Symbol();
const familyname = Symbol();
const obj = {
[name] : "太郎",
[familyname] : "山田",
fullname:function(){
console.log( this[familyname] + " " + this[name]);
}
};
obj.fullname(); // 結果: 山田 太郎
これでシンボルの name と familyname を外部から参照できないようにすれば、外部からアクセスできないプロパティの完成です。
今回はクロージャでシンボルを隠蔽してみます。
const makeObj = ( name , familyname ) =>{
const nameSymbol = Symbol();
const familynameSymbol = Symbol();
return {
[nameSymbol] : name,
[familynameSymbol] : familyname,
fullname:function(){
console.log( this[familynameSymbol] + " " + this[nameSymbol]);
}
}
};
const obj = makeObj("太郎","山田");
obj.fullname(); // 結果 山田 太郎
console.log( obj[nameSymbol] ); // // ReferenceError: nameSymbol is not defined
makeObjから返されたオブジェクトのfullnameメソッド内では、 [nameSymbol] と [familynameSymbol] にアクセスできます。
しかし、外部から [nameSymbol] を参照しようとするとエラーになります。
これで外部から参照できないプロパティが作成できました。
しかし上の例は、無駄が多いです。
整理すると次のようになります。
const makeObj = ( name , familyname ) =>{
const nameValue = name;
const familynameValue = familyname;
return {
fullname:function(){
console.log( familynameValue + " " + nameValue);
}
}
};
const obj = makeObj("太郎","山田");
obj.fullname(); // 結果 山田 太郎
シンボルとオブジェクト内のプロパティが消えてしまいました。
この例ではシンボル必要なかったですね。
他の例を考えてみたところ、全て同じような結論になります。
シンボルを外部からアクセスできないプロパティの作成目的で使用するのは、少し効率が悪いようです。
定数として利用
最後の使用目的は、定数利用です。
const FLG1 = Symbol();
const FLG2 = Symbol();
let data = FLG2;
if( data === FLG1 ){ console.log("flg1"); }
if( data === FLG2 ){ console.log("flg2"); } // こちらが適用される
FLG1とFLG2という定数を作成しておき、変数を比較します。
次のように数値を割り振るのでもよさそうな気がします。
const FLG1 = 1;
const FLG2 = 2;
しかし定数が増えていくと入力ミスで数値が重複する可能性があるので、 Symbol()の方が安全といえます。
ただし、Symbol()は非常に使いにくいです。
次のコードは、期待通りに動作しません。
let data = FLG2;
switch ( data ){
case FLG1:
console.log( "flg1");break; // こちらが適用される
case FLG2:
console.log( "flg1");break;
}
switch文により data は、文字列"Symbol()"として評価されます。
case FLG1: も文字列"Symbol()"として評価されるので、最初のcaseが適用されてしまうのです。
つまりSymbol()で作成した定数は、===での比較しかできません。
非常に使いにくいです。
シンボルは列挙できない
JavaScriptはオブジェクト内のプロパティを列挙する方法が用意されています。
しかしオブジェクト内のシンボルは、除外されます。
const data2 = Symbol();
const obj = {
data1 : "abc",
[data2]: "efg"
};
Object.keys(obj).forEach(e=>console.log(e)); // data1 のみ出力
for (let e in obj) console.log(e); // data1 のみ出力
全てのプロパティをforEachで処理したいというとき、シンボルを使用していると後で困ることになります。
用途をしっかりと検討してからシンボルを使用する必要がありますね。
ただしObject.assign()でコピーは可能です。
const obj2 = Object.assign({}, obj);
console.log(obj2[data2]); // 結果 efg
シンボルはJSONに変換できない
オブジェクトをファイルに保存したりサーバーに転送するとき、JSONに変換すると簡単です。
しかしシンボルはJSONに変換できません。
const data2 = Symbol();
const obj = {
data1 : "abc",
[data2]: "efg"
};
const json = JSON.stringify(obj);
console.log(json); // 結果 {"data1":"abc"}
これまた使いにくいです。
シンボルをJSONに変換したいときは、いろいろと小細工をする必要があります。
例えば次のようにシンボルを抜き出したオブジェクトを作成して、元のオブジェクトと一緒にラップします。
const jsonPre = {
obj:obj,
symbol:{
data2:obj[data2]
}
}
const json2 = JSON.stringify(jsonPre);
console.log(json2); // {"obj":{"data1":"abc"},"symbol":{"data2":"efg"}}
シンボルを列挙できないので、シンボルが複数あるときはひとつづつ手作業で入力していく必要があります。
そのため全てのオブジェクトで使用できる汎用的な処理にはなりません。
オブジェクトに戻すときは、再度シンボルをセットします。
const symbolMap = {
data2 : data2
}
const obj3 = JSON.parse(json2);
const obj4 = obj3.obj;
Object.keys(obj3.symbol).forEach(
key => obj4[symbolMap[key]] = obj3.symbol[key]
);
少しわかりにくいですが、symbolMap内の「data2 : data2」は、最初がプロパティ名で後ろがシンボル値です。
json2をオブジェクトに戻した後にobjを抜き出し、シンボルをセットしています。
symbolMapがないと、objにひとつづつシンボルをセットしていく必要があります。
しかし結局のところ、symbolMapに全てのシンボルを記述してないといけないわけで、全てのオブジェクトで使用できる汎用的な処理にはなりません。
やはり使いにくいですね。
グローバルシンボル
JavaScriptにはグローバルシンボルというものが用意されています。
これは文字列をキーとして、グローバルで使用できるシンボルを定義するものです。
グローバルシンボルの定義と取得
次のように Symbol.for() メソッドを使用して定義します。
const grobalSymbol = Symbol.for(文字列);
文字列に対応するでグローバルシンボルが作成されていない場合、新規でグローバルシンボルを作成します。
作成されている場合、対応するでグローバルシンボルを返します。
この仕様により一度定義したグローバルシンボルは、値が上書きされません。
例えば、次のように使用します。
Symbol.for("mySymbol"); // グローバルシンボルの作成
const mySymbol = Symbol.for("mySymbol"); // グローバルシンボルを取得する
ブラウザ上などの同一アプリケーションで複数のスクリプトが動いていても、グローバルシンボルが上書きされることがないので問題なく動作します。
グローバルシンボルから対応する文字列を取得
Symbol.keyFor()メソッドを使用すると、グローバルシンボルから対応する文字列を取得できます。
const mySymbol = Symbol.for("mySymbol"); // グローバルシンボルの作成
console.log( Symbol.keyFor(mySymbol) ); // 結果: mySymbol
ただし、「シンボルとは」の最後で紹介したdescriptionプロパティでも同じ結果を得ることができます。
console.log( mySymbol.description ); // 結果: mySymbol
逆にSymbol.keyFor() の引数に通常のシンボルを指定すると結果がundefinedになるので、シンボルがグローバルかどうかの確認に使用できます。
const symbol = Symbol("abc");
console.log( Symbol.keyFor(symbol) ); // 結果: undefined → グローバルではない
グローバルシンボルの使い道
複数のスクリプト間で同じオブジェクトを参照し、そのオブジェクト内でシンボルを使用されているときグローバルシンボルの意味があります。
それ以外は、グローバルシンボルを使用するメリットがありません。
<script>
const globalData = (()=>{
const symbol = Symbol.for("abc"); // グローバルシンボル作成
return { // シンボルをプロパティにもつオブジェクトを返す
[symbol] : 123
}
})();
</script>
<script>
(()=>{
const symbol = Symbol.for("abc"); // グローバルシンボルを取得
console.log( globalData[symbol] ); // 結果: 123
})();
</script>
しかし、上の例ではシンボルを使用する理由があまりありません。
知っていれば誰でもアクセスできるなら、通常のプロパティと同じです。
わざわざシンボルにする必要がありません。
グローバルシンボルもあまり使い道がないですね。
シンボルを使うケースはあまりない
シンボルは、もっと効率的な他の方法があり、有効な使い方はあまりありません。
JavaScriptには組み込みシンボルというものがあります。
実際のところ、このシンボルを定義するためにシンボルという機能があるのが実情です。
更新日:2023/07/20
関連記事
スポンサーリンク
記事の内容について

こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。