俺、サービス売って家買うんだ

Swift, Kotlin, Vue.js, 統計, GCP / このペースで作ってればいつか2-3億で売れるのがポっと出来るんじゃなかろうか

【今更ES2015を追ってみるシリーズ 3】Symbol編

f:id:ie-kau:20151019153301j:plain

不定期連載その3です。
今回はSymbolについて調べたのでその辺を書きます。

前回までの記事

Symbolとは?

ES2015から追加された新しいプリミティブ型です。目的としては以下が考えられます。

主目的

  • アプリケーション内のオブジェクトで利用する一意のキーを作成する
    • 何かしらの名前として利用するもの

副次効果

  • オブジェクトのアクセスコントロールの観点からprivateなプロパティを作成する
    • 後述しますが完全なprivateというわけにはいかないよう

公式によると

  • The Symbol type is the set of all non-String values that may be used as the key of an Object property (6.1.7).
  • Each possible Symbol value is unique and immutable.
  • Each Symbol value immutably holds an associated value called "Description" that is either undefined or a String value.

ECMAScript 2015 Language Specification – ECMA-262 6th Edition

  • キーとして利用する
  • 変更不可(イミュータブル)で一意な値

といったところですね。

作り方

var sym1 = Symbol();
var sym2 = Symbol('hoge');

hogeはdescription(説明)なのでオプションです。sym1.toStringしたら初期化したきのdescriptionが読めます。

これはTypeError

var sym = new Symbol('hoge');

使い方

オブジェクトのキーにした例です。下記例の場合当然ですがobj['hoge']ではアクセスできません。

var sym = Symbol('hoge');
var obj = {};

obj[sym] = 'fuga'
console.log(obj[sym]); // fuga
console.log(obj['fuga']); // undefined

同じdescriptionで初期化しても別のキーとして扱われます。(やらないけど)

var sym1 = Symbol('hoge');
var sym2 = Symbol('hoge');
var obj = {};

obj[sym1] = 'fuga'
obj[sym2] = 'piyo'
console.log(obj[sym1]); // fuga
console.log(obj[sym2]); // piyo

for..in構文で回してもキーはでてきません

var sym1 = Symbol();
var sym2 = Symbol();
var obj = {};

obj[sym1] = 'fuga'
obj[sym2] = 'piyo'
obj['foo'] = 1;

for (var key in obj) {
  console.log(key); // foo
}

Object.keysでも出てこない

// 略
console.log(Object.keys(obj)); // Array [ "foo" ]

プライベートプロパティを作る

Symobolをキーにしたオブジェクトの動作を応用することで簡単にprivateメンバーを持つオブジェクト作成することができます。

var Person = (function() {
    var nameSymbol = Symbol('name');

    function Person(name) {
        this[nameSymbol] = name;
    }

    Person.prototype.getName = function() {
        return this[nameSymbol];
    };

    return Person;
}());

var p = new Person('John');
console.log(p.getName()); // John
delete p.name
console.log(p.getName()); // John

※出典 - Private properties in JavaScript — Curiosity driven

なのですが... Object.getOwnPropertySymbols(object)というメソッドでobject内に含まれるキーとなっているSymbolを配列形式で取得することができます。つまり、下のコードのようにSymbolを取得してdeleteすることができるので完全にはprivateになっているわけではなさそうです。

var Person = (function() {
    var nameSymbol = Symbol('name');

    function Person(name) {
        this[nameSymbol] = name;
    }

    Person.prototype.getName = function() {
        return this[nameSymbol];
    };

    return Person;
}());

var p = new Person('John');
var pSym = Object.getOwnPropertySymbols(p)[0];
delete p[pSym];
console.log(p[pSym]); // undefined

Global領域で利用する

Global領域で利用するには以下の2つのメソッドを利用します。 またこの方法で作成されたSymbolはGlobalSymbolRegistryに登録され複数のGlobalにまたがるコードの実行環境全てで共有されます。

  • Symbol.for('key名');
    • 登録
  • Symbol.keyFor(symbol);
    • 利用

実行環境全てで共有されというのがどういうことかと言うと、ブラウザでいうと親ウィンドウとフレームでデータを共有することができます。

親ウィンドウ

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title></title>
</head>
<body>
<iframe id="frame" src="test2.html"></iframe>
<script>
(function() {
  var frame = document.getElementById('frame');
  var sym = Symbol.for('hoge');
  frame.addEventListener('load', function() {
    console.log(Symbol.keyFor(sym))
    console.log(frame.contentWindow.sym === sym); // ここがtrueになる
  });
})();
</script>
</body>
</html>

フレーム

<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title></title>
</head>
<body>
<script>
window.sym = Symbol.for('hoge');
</script>
</body>
</html>

Well-Known Symbols

Well-Known SymbolsはES組み込みのシンボルであり、ES内部のメソッドであったりプリミティブ値を表しています。
特定のオブジェクトを拡張する際のプロパティのキーに利用することが典型的な使い方のようです。

例を見てみましょう。

var obj = {
  a: 1,
  b: 2,
  c: 3, 
  [Symbol.iterator]() {
    var i = 0;
    var keys = Object.keys(this);
    var keyLen = keys.length;
    var that = this;

    return {
      next() {
        var ret = (i <= keyLen) ? {value: that[keys[i]], done: false} : {value: undefined, done: true};
        i++;
        return ret;
      }
    }
  }
}

for (var i of obj) {
  console.log(i);
}

「for..in文でループしろよ」というツッコミはさておき、上記の例ではobjを拡張してfor..of文でループできるようにしています。
iteratorに関する細かい説明はまた書きますが、for..of文のES内部の動作は[Symbol.iterator]メソッドをコールそのメソッドが返すiteratorが持つnextメソッドをコールします。そのため[Symbol.iterator]且つそのメソッドが返すiteratorに含まれるnextメソッドが実装されていない状態のObjectではfor..of文を利用することができません。

つまることろ上記の[Symbol.iterator]はインターフェイスに近いかたちになります。ES2015公式の仕様書には書かれていませんがiterator protocolといたるところで説明されているのはそのためです。

こことか。
Iteration protocols - JavaScript | MDN

@@表記について

Well-Known Symbolsについてい調べていると@@toPrimitiveの様に@@付きの表記で書かれている説明をよく目にします。 最初protocolとしてオブジェクトに組み込むときにこの書式で実装できるようになるのかと思ったのですがどうやら仕様書上だけの表記で単にWell-known Symbolでかあることを明示的にしてあるだけの話でした。

stackoverflow.com

stackoverflow.com

まとめ

Symbolでキーを作成するとなるとES2015が主流なれば、オブジェクト作成時の書き方がかなり変わってきそうですね。ただ、そうなるとリテラルで書けるようになってほしいですが。 またWell-known Symbolを利用してオブジェクトに様々な拡張ができるようになればより表現の幅が広がってきそうなイメージを得られます。(いよいよプログラマとしてのスキルがついていかない...)

その他参考