8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

JavaScriptのSymbolについて

Last updated at Posted at 2022-08-30

初めに

今回はSymbolの基本、そして一部のメソッドの使い方をまとめてみました。

参考文章はこちらです。

中国語が分かる方はこちらもおすすめです。

Memo

Symbolの使用について
  • Symbolはプリミティブであり、一意の値を表します。
  • Symbolで創られたすべての値はユニック(unique)で、見た目は同じでも違う存在として判定されます。
  • Symbolはオブジェクトのプロパティ名に用いるとき、[]でアクセスしなければなりません。
Symbolへの反復処理について
  • for...infor...ofを使ってSymbolプロパティを列挙したり反復処理したりできません。
  • Symbolはプライベートプロパティではないが、Object.keys()Object.getOwnPropertyNames()JSON.stringify()に返されることができません。
  • Object.getOwnPropertySymbols()に特定のオブジェクトから見つかるすべてのSymbolプロパティを返す。
  • 静的メソッドReflect.ownKeys()なら、指定のオブジェクトすべてのプロパティキー(Symbolも)を返す。

プロパティがオブジェクトの内部に使ってほしい場合は、Symbolは普通の反復処理メソッドでアクセスできない特性を利用すると便利です。(プライベートプロパティではないけど。)

Symbols

Symbolnewを使わない。呼び出しに値を入れる、あるいはオブジェクトの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...infor...of
プロパティの配列を返すメソッド、SymbolObject.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) ]

SymbolObject.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はグローバルに値を登録する、という特性を利用してグローバルにプロパティを創り、シングルトンを値としてそのプロパティにアサインしたら、このグローバルプロパティがどこで呼び出されても同じ参照(シングルトン)を返してくれます。

moudle.js
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;

上のグローバルプロパティをインポートしたら、今のファイルにも使えるようになる。

app.js
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()に変えたら、プロパティへのアクセスができなくなって、書き換えられるわけでもなくなります。

moudle.js
const fooKey = Symbol('foo');
// following the same procedure
app.js
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

bcはインスタンスとしてプロトタイプチェーンから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メソッドはhintstringnumberdefaultのどれかを返してくれます。なのでそれぞれの値によって、各自の処理を行うメソッドをカスタマイズできます。

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メソッドはほかにまだたくさんあるのですが、今回はとりあえずこれぐらいにしておきたいと思います。

8
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?