初めに
今回はSymbol
の基本、そして一部のメソッドの使い方をまとめてみました。
参考文章はこちらです。
中国語が分かる方はこちらもおすすめです。
Memo
Symbolの使用について
-
Symbol
はプリミティブであり、一意の値を表します。 -
Symbol
で創られたすべての値はユニック(unique)で、見た目は同じでも違う存在として判定されます。 -
Symbol
はオブジェクトのプロパティ名に用いるとき、[]
でアクセスしなければなりません。
Symbolへの反復処理について
-
for...in
、for...of
を使ってSymbol
プロパティを列挙したり反復処理したりできません。 -
Symbol
はプライベートプロパティではないが、Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
に返されることができません。 -
Object.getOwnPropertySymbols()
に特定のオブジェクトから見つかるすべてのSymbol
プロパティを返す。 - 静的メソッド
Reflect.ownKeys()
なら、指定のオブジェクトすべてのプロパティキー(Symbol
も)を返す。
プロパティがオブジェクトの内部に使ってほしい場合は、Symbol
は普通の反復処理メソッドでアクセスできない特性を利用すると便利です。(プライベートプロパティではないけど。)
Symbols
Symbol
はnew
を使わない。呼び出しに値を入れる、あるいはオブジェクトのtoString()
メソッドが返した値をシンボル値を生成します。
let id = Symbol('id');
console.log(id); // Symbol(id) // Symbol
const obj = {
toString() {
return 123;
},
};
const sym = Symbol(obj);
console.log(sym); // Symbol(123)
一意であるため、中身の同じ説明文(値)が同じでも判定はいつもfalse
になります。
let id1 = Symbol('id');
let id2 = Symbol('id');
console.log(id1 === id2); // false
シンボル値は文字列に自動変換ができない。toString()
かString()
でそのまま出力する。中身の値を参照したいとき、description
というプロパティを利用して表示します。
let id = Symbol('id');
console.log(id); // Symbol(id) // Symbol
console.log(id.toString()); // Symbol(id) // string
console.log(String(id)); // Symbol(id) // string
console.log(id.description); // id // string
オブジェクトに既存のプロパティがあったとしても、シンボル値の説明文が独自の固有識別子を持つので衝突しません。
let id = Symbol('id');
let id1 = Symbol('id');
let user1 = {
name: 'Mick',
id: 1,
};
user1[id] = 1;
user1[id1] = 2;
console.log(user1)
// { name: 'Mick', id: 1, [Symbol(id)]: 1, [Symbol(id)]: 2 }
(['id']
という書き方がid
という文字列をプロパティ名として使うが、[id]
は変数id
の値Symbol(id)
を入れるということです。)
ほかの書き方↓は、オブジェクトに[]
中に変数入れる、またはObject.defineProperty()
で定義する。
let id = Symbol('id');
let user2 = {
name: 'Lucy',
[id]: 2,
};
let user3 = {};
Object.defineProperty(user3, id, { value: 3 });
console.log(user3[id]); // 3
もちろんプロパティ名だけではなく値としても入れます。
const log = {};
log.level = {
DEBUG: Symbol('debug'),
INFO: Symbol('info'),
WARN: Symbol('warn'),
};
console.log(log.level.DEBUG.description); // debug
Example1 switch
Symbol
はユニックを値を生み出すので、switch
などで唯一のケースが通せたいときとてもやりやすいです。
const colorRed = Symbol();
const colorGreen = Symbol();
function getColor(color) {
switch (color) {
case colorRed:
return 'Red';
case colorGreen:
return 'Green';
default:
throw new Error('Undefined color');
}
}
console.log(getColor(colorRed)); // Red
const shapeType = {
triangle: Symbol(),
};
function getArea(shape, { width, height } = {}) {
let area = 0;
switch (shape) {
case shapeType.triangle:
area = 0.5 * width * height;
break;
}
return area;
}
console.log(getArea(shapeType.triangle, { width: 100, height: 100 })); // 5000
衝突しない利点を応用したらいろいろできそうです!
Hidden Properties
Symbol
プロパティは、
プロパティの反復処理メソッド、for...in
、for...of
、
プロパティの配列を返すメソッド、Symbol
はObject.keys()
、Object.getOwnPropertyNames()
、
そしてJSONへの置き換えJSON.stringify()
では表しません。
下は一部の例です。
let a = Symbol('foo');
let b = Symbol('bar');
let obj = {
[a]: 'foo',
[b]: 'bar',
};
for (let prop in obj) {
console.log(prop); // nothing happens
};
console.log(Object.getOwnPropertyNames(obj)); // []
console.log(Object.getOwnPropertySymbols(obj)); // [ Symbol(foo), Symbol(bar) ]
console.log(Reflect.ownKeys(obj)); // [ Symbol(foo), Symbol(bar) ]
Symbol
はObject.keys()
とObject.getOwnPropertyNames()
から勝手に返されないので隠せる。
容易に外に出さない、それに内部メソッドに提供するプロパティを創るときにSymbol
は便利だと思います。(プライベートプロパティではないけど。)
let size = Symbol('size');
class Collection {
constructor() {
this[size] = 0;
}
add(item) {
// this[this[size]] => this.0 // this[size] = 0, ...etc.
this[this[size]] = item;
console.log(this[this[size]]);
this[size] += 1;
}
static sizeOf(instance) {
return instance[size];
}
}
let x = new Collection();
console.log(Collection.sizeOf(x)); // 0
console.log(Object.getOwnPropertySymbols(x)); // [ Symbol(size) ]
console.log(x); // Collection { [Symbol(size)]: 0 }
x.add('foo');
console.log(Collection.sizeOf(x)); // 1
console.log(Object.getOwnPropertySymbols(x)); // [ Symbol(size) ]
console.log(x); // Collection { '0': 'foo', [Symbol(size)]: 1 }
上は新しいプロパティ創るとき、プロパティ[Symbol(size)]
持っている値がプロパティを作っていきます。(このやり方は一応indexの効果もあると思う。)
console.log(Object.keys(x)); // [ '0' ]
console.log(Object.getOwnPropertyNames(x)); // [ '0' ]
Methos
Symbol.for()
Symbol()
はユニックで同じ説明文(description)でも異なる。
しかし同じ説明文のシンボルが一つの参照にしたいときもあって、Symbol.for()
はSymbol()
と違って、生成されたシンボル値はglobal symbol registryに保存しているので、同じ説明文のシンボルがあればそれを返す、ない場合は生成するというメソッドです。
let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');
console.log(s1 === s2);
//
let s1 = Symbol.for('foo');
{
let s2 = Symbol.for('foo');
console.log(s1 === s2); // true
};
(Symbol.for
は別のスコープでもシンボル値をグローバルに登録する。)
Symbol.keyfor()
Symbol.for()
はグローバル環境にシンボル値を登録することに対し、Symbol.keyfor()
はそれを見つかったら登録されたシンボル値を返します。
Symbol()
はそうではありません。Symbol()
は毎回のコールは新しいシンボル値を生成し、そしてグローバル環境にも登録しません。
let s1 = Symbol.for('foo');
console.log(Symbol.keyFor(s1)); // foo
let s2 = Symbol('foo');
console.log(Symbol.keyFor(s2)); // undefined
Example2 Singleton
シングルトンは単一のインスタンスのことに指します。何度も同じインスタンスを創るより、すべてのインスタンスが一つのオブジェクトに参照したらメモリーにやさしいです。
Symbol.for
はグローバルに値を登録する、という特性を利用してグローバルにプロパティを創り、シングルトンを値としてそのプロパティにアサインしたら、このグローバルプロパティがどこで呼び出されても同じ参照(シングルトン)を返してくれます。
const fooKey = Symbol.for('foo');
function A() {
this.foo = 'hello';
}
// if global[Symbol.for('foo')] doesn't exist, set property and value
if (!global[fooKey]) {
global[fooKey] = new A();
}
const globalSymbol = global[fooKey];
export default globalSymbol;
上のグローバルプロパティをインポートしたら、今のファイルにも使えるようになる。
import globalSymbol from "./symbol.js";
console.log(globalSymbol); // A { foo: 'hello' }
console.log(global[Symbol.for('foo')]); // A { foo: 'hello' }
console.log(globalSymbol === global[Symbol.for('foo')]); // true
global[Symbol.for('foo')] = { foo: 'world' };
console.log(global[Symbol.for('foo')]); // { foo: 'world' }
console.log(globalSymbol === global[Symbol.for('foo')]); // false
しかし、これでは肝心な値がいつでも書き換える。
そこで、キーが必ず一意のシンボル値を生成するSymbol()
に変えたら、プロパティへのアクセスができなくなって、書き換えられるわけでもなくなります。
const fooKey = Symbol('foo');
// following the same procedure
console.log(globalSymbol); // A { foo: 'hello' }
console.log(global[Symbol.for('foo')]); // undefined
console.log(globalSymbol === global[Symbol.for('foo')]); // false
(毎回このスクリプトを実行するとシンボル値違いますが。)
Symbol methods in Class
& Object
[Symbol.hasInstance]
[Symbol.hasInstance]
は、オブジェクト内部メソッドで、instanceof
運算子と遭遇したらメソッドを実行する。
下はクラスで[1, 2, 3] instanceof Array
を実装してみたコードです。
class Myclass {
[Symbol.hasInstance](instance) {
return instance instanceof Array;
}
}
console.log([1, 2, 3] instanceof new Myclass()); // true
カスタマイズも可能です。
class Even {
static [Symbol.hasInstance](item) {
return Number(item) % 2 === 0;
}
}
console.log(1 instanceof Even); // false
console.log(2 instanceof Even); // true
//
const Even = {
[Symbol.hasInstance](item) {
return Number(item) % 2 === 0;
}
};
console.log(1 instanceof Even); // false
console.log(2 instanceof Even); // true
[Symbol.hasInstance]
がinstanceof
に触発される特性を用いてclass Even
は入れられた値の処理して返す。オブジェクトEven
も同じです。
(class Even
ではstatic
をつけて内部の静的メソッドとして使われるので、new Even()
ではこのカスタマイズされた[Symbol.hasInstance]
メソッド使えません。)
[Symbol.species]
[Symbol.species]
はコンストラクタの参照先を返す。インスタンスを創るとこの[Symbol.species]
が使われます。
class MyArray extends Array {
}
const a = new MyArray(1, 2, 3);
const b = a.map(x => x);
const c = a.filter(x => x > 1);
console.log(b instanceof MyArray); // true
console.log(c instanceof MyArray); // true
b
とc
はインスタンスとしてプロトタイプチェーンからArray
のメソッドをアクセスするので、どれもclass MyArray
のインスタンスでした。
このときの[Symbol.species]
はこんな感じです。(デフォルト状態)
class MyArray extends Array {
static get [Symbol.species]() {
return this;
}
}
そして[Symbol.species]
にArray
を返してもらってみたら、
class MyArray extends Array {
static get [Symbol.species]() {
return Array;
}
}
const a = new MyArray(1, 2, 3);
const b = a.map(x => x);
const c = a.filter(x => x > 1);
console.log(b instanceof MyArray); // false
console.log(c instanceof MyArray); // false
console.log(b instanceof Array); // true
console.log(c instanceof Array); // true
[Symbol.toPrimitive]
Symbol.toPrimitive
メソッドはhint
がstring
、number
、default
のどれかを返してくれます。なのでそれぞれの値によって、各自の処理を行うメソッドをカスタマイズできます。
let obj = {
[Symbol.toPrimitive](hint) {
switch (hint) {
case 'number':
return 123;
case 'string':
return 'str';
case 'default':
return 'default';
default:
throw new Error('It is not number, string, or default.');
}
}
};
console.log(10 * obj); // 1230
console.log(3 + obj); // 3default
console.log('123' + obj); // 123default
console.log(String(obj)); // 'str'
console.log(obj == 'default');
// Error: It is not number, string, or default.
[Symbol.iterator]
[Symbol.asyncIterator]
...Symbol
メソッドはほかにまだたくさんあるのですが、今回はとりあえずこれぐらいにしておきたいと思います。