uhyohyo.net

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

十六章第四回 シンボル

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

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

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

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

var s = Symbol();

シンボルの使い方

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

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

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

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

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

これを実行するとhoge piyoと表示され、ちゃんとsがキーになっていることが分かります。

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

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

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

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

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

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

undefinedが表示され、sをキーにした場合とs.toString()をキーにした場合で異なることが分かります。このように、文字列以外をオブジェクトのキーにできるというのはいままでにない概念ですね。

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

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

このことを利用して、一六章第一回で紹介した例と似たことができます。そのときに下のような例がありました。

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

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

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


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

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

リテラルを使えばどこでも任意のキーを作れる文字列とは違い、キーがシンボルならば、外部からこのmyhiddensymbolと同じシンボルを作る方法はありません。

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

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

Object.keysやObject.getOwnPropertyNames(十一章第四回)を使えばオブジェクトのプロパティ名を全て得ることができるので、これを使えばオブジェクトのキーとなっているシンボルも発見できそうですね。

ところが、実はこれらはシンボルを発見できません。文字列のキーのみ列挙して返します。

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

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

これを使えば結局見つかってしまうというわけです。

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

さらに、シンボルが追加された目的にはこれからの拡張性を考えてのこともあるでしょう。詳しくはこのあと少し紹介しますが、シンボルは作ると毎回異なるものが得られるという性質から、オブジェクトに新しいプロパティを追加するとき、シンボルをキーとして追加すれば、Object.getOwnPropertySymbolsを使わない限りは既存のコードと衝突することはありません。

Well-Known Symbols

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

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

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

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

Well-Known Symbolsには以下の種類があります。

それぞれどのような意味があるのかは追々紹介するとしましょう。

イテレータ

ところで、イテレータの話(一六章第二回)@@iteratorメソッドなるものを紹介しましたが、実はその実体はこのwell-known symbolです。つまり、@@iteratorというのはSymbol.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: null
        };
      }
      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"というプロパティ名になります。これは、文字列として表すことができないシンボルをキーとしたい場合に少し不便です。そこで、ES6では新しい記法が追加されました。[ ]で囲むことで、オブジェクトリテラル中のプロパティ名を式にできます。つまり、

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には第一引数として文字列を渡すことができます。これはそのシンボルの名前、というか説明を指定するものです。

var s = Symbol("foo");

注意すべきなのは、Symbolに同じ引数を渡して作ったシンボルでも、やはりそれぞれ異なるということです。

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

この文字列は、シンボルを文字列に変換したときに見ることができます。

console.log(Symfol("foo").toString()); //Symbol(foo)

ところで、Rubyなどのプログラミング言語では、シンボルと文字列が1対1に対応していて、シンボルのリテラルがあります。それと比べるとJavaScriptのシンボルは少し違って違和感があるかもしれません。どちらもプロパティの識別に使うという点では同じですが。実は、JavaScriptでもシンボルと文字列を対応させる方法があります。

それには、Symbol.forメソッドを使います。引数として文字列を渡すと、そのキーに対応するシンボルが返されます。

ただし、返されるシンボルは今まで扱ってきたシンボルと変わらない普通のシンボルで、本質的にその文字列と関わりがあるわけではありません。Symbol.forが特殊なのは、同じ文字列を渡すと同じシンボルが返されるという点だけです。


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

この動作は、その文字列が初めて渡された場合は通常通り新しいシンボルを作って返し、以前にも渡された文字列のときはそのときのシンボルをまた返すという形で行われます。

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

繰り返しますが、本質的にシンボルと文字列に関係があるわけではないので、例えば

var foo=Symbol.for("foo");

としても、obj[foo]obj["foo"]が等しくなるわけではありません。その点は注意しましょう。