uhyohyo.net

JavaScript初級者から中級者になろう

十六章第四回 シンボル

このページの最終更新日:

シンボルは、ES6で導入された新しいプリミティブです。

新しいプリミティブであるということは、オブジェクトではなく、文字列でも数値でも真偽値でもない値ということです。

ただ、シンボルは既存のプリミティブとは違い、リテラルによる表現を持ちません。文字列はコード中に"foo"とか文字列リテラルを書けば得られるし、数値や真偽値もそれを表すリテラルがあり、まさに「値」という感じがしました。シンボルはそういう表現を持ちません。

シンボルを作るには、Symbolという組み込み関数を呼び出します。


let s = Symbol();

シンボルの使い方

シンボルは、オブジェクトのプロパティのキー(プロパティ名)にすることができます

今までオブジェクトのプロパティ名は必ず文字列でした。それが、文字列ではなくシンボルでもいいということです。次の例を見てください。


const obj = {};
obj["foo"]="hoge";

const s = Symbol();
obj[s]="piyo";

console.log(obj.foo,obj[s]);

これを実行するとhoge piyoと表示され、obj[s]により"piyo"が取得できていることが分かります。

ただ、今までも配列など、見かけ上キーとして数値を指定するような場合もありました。その場合は文字列に変換されていましたね。例えば次の例です。


const obj={};
obj[3]="foo";

console.log(obj["3"]);

これにより、3をキーにした場合と"3"をキーにした場合で同じプロパティを参照していることがわかります。

一方、シンボルは文字列に変換されるわけではありません。次の例で確かめてみましょう。


const obj={}, s=Symbol();
obj[s]="foo";
console.log(obj[String(s)]);

undefinedが表示され、sをキーにした場合とString(s)をキーにした場合で異なることが分かります。String関数は渡されたものを文字列に変換する関数でしたね。

つまり、シンボルsがオブジェクトのキーとして使われた場合はあくまでシンボルのままオブジェクトのキーになっているのであり、文字列に変換されているわけではないということです、このように、文字列以外をオブジェクトのキーにできるというのはいままでにない概念ですね。

オブジェクトのキーとしては、シンボルはあらゆる文字列と異なります。すなわち、シンボルをキーとして作ったプロパティは、どんな文字列をキーとしても取得できません。逆も同様です。

なお、複数回Symbol()を呼び出すと、毎回異なるシンボルが返されます。シンボルはプリミティブですが、作らなければいけないという点、そして作ると毎回異なるという点でオブジェクトに似ていますね。

このことを利用して、WeakMapのときに紹介した例と似たことができます。下のような例がありました。


function saveValueInObject(obj,value){
  obj._myvalue=value;
}
function getValueFromObject(obj){
  return obj._myvalue;
}

適当なオブジェクトに勝手に値を結び付けたい場合の例ですね。これの問題点は、外部から勝手に_myvalueというプロパティをいじられたり、あるいは参照されたりする危険性があるということでした。

キーをシンボルにすればその危険性が下がります。


const myhiddensymbol=Symbol();  //実際はグローバル変数にせずにどこかに隠しておく

function saveValueInObject(obj,value){
  obj[myhiddensymbol]=value;
}
function getValueFromObject(obj){
  return obj[myhiddensymbol];
}

リテラルを使えばどこでも任意のキーを作れる文字列とは異なり、今回キーとして使ったmyhiddensymbolと同じシンボルを別のところで作る方法はありません。シンボルは作るたびに新しい新しいものができるからですね。

なので、外部から勝手にこのフィールドをいじることはできないだろうというわけです。

ただし、この方法には抜け穴があります。

まず思い出されるのは、Object.keysObject.getOwnPropertyNamesではないでしょうか。こういったメソッドを使えばオブジェクトのプロパティ名を全て得ることができるので、これを使えばオブジェクトのキーとなっているシンボルも発見できると思うかもしれません。

ところが、実はこれらはシンボルを発見できません。これらのメソッドは文字列のキーのみ列挙するのです。

その代わりに、ES2015の新しいメソッドObject.getOwnPropertySymbolsが存在して、これがオブジェクトのキーとなっているシンボルを全て列挙して返してくれます。


var obj={}, s=Symbol();
obj[s]="foo";
console.log(Object.getOwnPropertySymbols(obj)[0] === s); //true

これを使えば、上の例のmyhiddensymbolは結局見つかってしまうというわけです。

なお、Object.keysやObject.getOwnPropertyNamesではシンボルを発見できないという仕様は、一つには後方互換性を保つという目的があると考えられます。オブジェクトにシンボルをキーとするプロパティが存在しても、シンボルがない時代(ES5まで)のコードには発見できません。つまり、任意のオブジェクトに勝手にシンボルをキーとするプロパティが追加されていても動作に影響を与えません。

つまり、JavaScriptをバージョンアップするにあたって既存の機能を変更・追加したときに既存のES5プログラムが壊れたら困るので、ES5プログラムからは見えないシンボルを使って機能拡張を行ったということです。

Well-Known Symbols

さて、Well-Known Symbolsとは、特別なシンボルです。

特別なシンボルといっても何か特殊な機能を持つわけではなく、それ自体はただのシンボルです。

ただ、プログラムの実行の内部処理から参照されるという点で特別な意味を持ちます。

Well-Known Symbolsは予め作られたシンボルで、Symbolのプロパティとして参照可能です。例えばiteratorというwell-known symbolは、Symbol.iteratorとして参照可能です。

Well-Known Symbolsには今のところ以下の種類がありますが、言語仕様の追加に伴って増えるかもしれません。

それぞれどのような意味があるのかは追々紹介するとしましょう。主に、組み込み関数の動作をカスタマイズするために使うことができます。

well-knownシンボルの紹介があるものにはリンクが張ってありますが、どれもここより先の記事なので注意してください。

イテレータ

ところで、イテレータの話で@@iteratorメソッドなるものを紹介しましたが、実はその実体はこのwell-known symbolです。つまり、@@iteratorというのはSymbol.iteratorのことです。なんということはありません。プロパティ名がwell-known symbolときにいちいちSymbol.iteratorと書くのは大変なので代わりに@@iteratorと書いているだけの話だったのです。

つまり結局のところ、iterableなメソッドは、Symbol.iteratorをキーとして参照できるメソッドを持てばいいのです。例えば配列はiterableだったから、@@iteratorメソッドを持ちます(実際はArray.prototype[Symbol.iterator]に存在しています)。次のようにすれば確かめられるでしょう。


var arr=[1,2,3]; //適当な配列
console.log(arr[Symbol.iterator]);

何らかの関数があることが確認できたと思います。

iterableからイテレータを得る場合は、こうして得られるメソッドを呼び出してイテレータを得るのです。

そこで、とりあえずiterableを作ってみましょう。とりあえず、以前やったフィボナッチ数のイテレータを返すiterableをつくります。


var iterable={};
iterable[Symbol.iterator] = function(){
  //イテレータを返す
  return {
    a:1,
    b:0,
    next:function(){
      if(this.a>100){
        //無限ループ防止のため100を超えたら打ち切り
        return {
          done: true,
          value: undefined,
        };
      }
      var n=this.a+this.b, oldb=this.b;
      this.a=oldb, this.b=n;
      return {
        value: oldb,
        done: false,
      };
    }
  };
};

//ためしに回してみる
for(var n of iterable){
  console.log(n);
}

これでiterableが作れました。

ただし前にも説明したとおり、これはあまりよいiterableではありません。なぜなら、これはイテレータから出てくるデータがイテレータ自体に内包されていて、iterableがただの飾りになっているからです。本来は、データはiterableの中にあって、イテレータはそのデータを参照して順番に返す仕事をするだけでなければなりません。この例はiterableではなくイテレータがデータを持っているからだめですね。

ならばということで、ここで使われているaやbというプロパティをiterableのほうに移してみましょう。


var iterable={
  a:1,
  b:0
};
iterable[Symbol.iterator] = function(){
  //イテレータを返す
  return {
    iterable: this,
    next:function(){
      if(this.iterable.a>100){
        //無限ループ防止のため100を超えたら打ち切り
        return {
          done: true,
          value: null
        };
      }
      var n=this.iterable.a+this.iterable.b, oldb=this.iterable.b;
      this.iterable.a=oldb, this.iterable.b=n;
      return {
        value: oldb,
        done: false
      };
    }
  };
};

//ためしに回してみる
for(var n of iterable){
  console.log(n);
}

これはデータをiterableのほうが持っていて、イテレータはiterableの参照とnextメソッドだけになりました。

しかしこれならいいかというと、そうでもありません。これでは複数のイテレータを作ったときに動作が連動してしまいます。

イテレータは、どこまで進んだかという情報自体はイテレータが持っていなければならないのです。そのため、あるイテレータが途中まで読み進んでいたとしても、別のイテレータを作って回したら最初から順番に値が出てこなければなりません。

結論としては、フィボナッチ数という例がiterableに相応しくなかったわけですね。計算によって求められるので、参照すべきデータをiterable自体が持つことができません(同じ計算を省くためにメモしておくという方法はありますが)。

とにかくこれでiterableの作り方がわかりました。何か回せそうなものを作るときは、iterableにしてみるのがよいでしょう。

この例に、well-known symbolsが特殊な意味を持つということが表れています。for-of文などがiterableからイテレータを得るときに、内部的に@@iteratorメソッドを呼び出しているのです。このようにwell-known symbolsは処理系から使用されるという意味で特殊で、これらを使うことによりオブジェクトの挙動(@@iteratorの場合はどのようなイテレータを返すかという挙動)を制御することができるわけです。他のwell-known symbolsもそれぞれ処理系により使用される場面があります。

補足

シンボルはプリミティブなのですが、typeof演算子を使うとどうなるのでしょう。

実は、"symbol"が返ります。新しい値ですね。


console.log(typeof Symbol());  //symbol

また、さっきからシンボルをキーとするオブジェクトを作るのに、いちいち


var obj={}, s=Symbol();
obj[s]="foo";

のように、空のオブジェクトを作ってからシンボルをキーにして値を代入するということをしていました。これは、オブジェクトリテラルでプロパティを作るやり方だと文字列をキーとするプロパティになってしまうからです。


var s=Symbol(), obj= {
  s:"foo"
};

console.log(obj.s); //foo

ここでのオブジェクトリテラル中のプロパティ名sは、当然ながら変数sではなく文字列"s"というプロパティ名になります。これは、文字列として表すことができないシンボルをキーとしたい場合に少し不便です。そこで、ES2015では新しい記法が追加されました。[ ]で囲むことで、オブジェクトリテラル中のプロパティ名を式にできます。つまり、


var s=Symbol(), obj= {
  [s]: "foo"
};

console.log(obj[s]);  //foo

この記法により、文字列"s"ではなく、変数sの中身をキーとするプロパティを作ることができます。ちなみに、これはシンボルに限らず任意の式を入れることができます。例えば、


var x="foo", y="bar";
var obj={
  [x+y]: 3
};
console.log(obj.foobar);  //3

これはなかなか便利ですね。使う機会もあることでしょう。

また、実はSymbolには第1引数として文字列を渡すことができます。これはそのシンボルの名前、あるいは説明を指定するものです。


var s = Symbol("foo");
console.log(s) // Symbol(foo)

これにより、シンボルをconsole.logで変換したりあるいは文字列に変換したときに名前が表示されて分かりやすくなります。

注意しておくと、このようにSymbolに名前を付けたとしても、作られるのは毎回新しいシンボルです。よって、Symbolに同じ引数を渡して作ったシンボルでも、それぞれは異なります。


console.log(Symbol("foo") !== Symbol("foo"));  //true

Symbol.for

最後にSymbol.forメソッドを紹介しておきます。これは文字列に対してシンボルを登録しておける機能です。正直どこに需要があるのかいまいち分かりませんが、あるので紹介しておきます。

Symbol.forの引数に文字列を渡すと、やはりシンボルが作られて返されます。Symbol.forが特別なのは、このとき渡された文字列に対していま返されたシンボルが登録されるという点です。もしもう一度同じ文字列をSymbol.forに渡すと、新しいシンボルが作られる代わりに以前同じ文字列のときに作ったシンボルが返されます。


var a=Symbol.for("foo");
var b=Symbol.for("foo");
console.log(a===b); //true

この例では1回目のSymbol.forの呼び出しで新しいシンボルが作られましたが、2回目の呼び出しでは引数が前と同じ"foo"なので前と同じシンボルが返ります。その結果、aとbには同じシンボルが入っていることになります。

なお、例えばSymbol.for("foo")によって作られたシンボルはSymbol("foo")の場合と同様に名前が付けられたシンボルとなります。

どこに需要があるのかいまいち分からないと述べた理由は、シンボルに名前を付けて管理したければこんな感じで適当なオブジェクトに入れておけば自分で管理できそうだからです。


const dict = {
  "foo": Symbol("foo"),
  "bar": Symbol("bar"),
};

なお、Symbol.forで得られたシンボルから元の文字列を得る方法もあります。Symbol.keyForにそのシンボルを渡して呼び出すと文字列が返ってきます。Symbol.for以外で作ったシンボルを渡しても対応する文字列はないのでundefinedが返ります。