シンボルは、ファクトリ関数 Symbol()
を介して作成されるプリミティブ値です。
const mySymbol = Symbol('mySymbol');
パラメータはオプションで、説明を提供します。これは主にデバッグに役立ちます。
シンボルはプリミティブ値です。
typeof
を使用して分類する必要があります。
const sym = Symbol();
.equal(typeof sym, 'symbol'); assert
オブジェクトのプロパティキーにすることができます。
const obj = {
: 123,
[sym]; }
シンボルはプリミティブ値ですが、Symbol()
によって作成された各値は一意であり、値によって比較されないという点で、オブジェクトのような側面も持ちます。
> Symbol() === Symbol()false
シンボルが登場する前は、一意な(自分自身とのみ等しい)値が必要な場合、オブジェクトが最良の選択肢でした。
const string1 = 'abc';
const string2 = 'abc';
.equal(
assert=== string2, true); // not unique
string1
const object1 = {};
const object2 = {};
.equal(
assert=== object2, false); // unique
object1
const symbol1 = Symbol();
const symbol2 = Symbol();
.equal(
assert=== symbol2, false); // unique symbol1
シンボルファクトリ関数に渡すパラメータは、作成されたシンボルの説明を提供します。
const mySymbol = Symbol('mySymbol');
説明には、2つの方法でアクセスできます。
まず、.toString()
によって返される文字列の一部です。
.equal(mySymbol.toString(), 'Symbol(mySymbol)'); assert
次に、ES2019以降、プロパティ .description
を介して説明を取得できます。
.equal(mySymbol.description, 'mySymbol'); assert
シンボルの主なユースケースは次のとおりです。
赤、オレンジ、黄、緑、青、紫の色を表す定数を作成したいとします。簡単な方法の1つは、文字列を使用することです。
const COLOR_BLUE = 'Blue';
プラス面としては、その定数をログに記録すると、役立つ出力が生成されます。マイナス面としては、内容が同じ2つの文字列は等しいと見なされるため、無関係な値を色と間違えるリスクがあります。
const MOOD_BLUE = 'Blue';
.equal(COLOR_BLUE, MOOD_BLUE); assert
この問題は、シンボルを使用して解決できます。
const COLOR_BLUE = Symbol('Blue');
const MOOD_BLUE = Symbol('Blue');
.notEqual(COLOR_BLUE, MOOD_BLUE); assert
シンボル値の定数を使用して関数を実装してみましょう。
const COLOR_RED = Symbol('Red');
const COLOR_ORANGE = Symbol('Orange');
const COLOR_YELLOW = Symbol('Yellow');
const COLOR_GREEN = Symbol('Green');
const COLOR_BLUE = Symbol('Blue');
const COLOR_VIOLET = Symbol('Violet');
function getComplement(color) {
switch (color) {
case COLOR_RED:
return COLOR_GREEN;
case COLOR_ORANGE:
return COLOR_BLUE;
case COLOR_YELLOW:
return COLOR_VIOLET;
case COLOR_GREEN:
return COLOR_RED;
case COLOR_BLUE:
return COLOR_ORANGE;
case COLOR_VIOLET:
return COLOR_YELLOW;
default:
throw new Exception('Unknown color: '+color);
}
}.equal(getComplement(COLOR_YELLOW), COLOR_VIOLET); assert
オブジェクトのプロパティ(フィールド)のキーは、2つのレベルで使用されます。
プログラムは *基本レベル* で動作します。そのレベルのキーは、*問題領域*(プログラムが問題を解決する領域)を反映します。たとえば、
ECMAScriptと多くのライブラリは、*メタレベル* で動作します。問題領域の一部ではないデータの管理とサービスの提供を行います。たとえば、
標準メソッド .toString()
は、オブジェクトの文字列表現を作成するときにECMAScriptによって使用されます(行A)。
const point = {
x: 7,
y: 4,
toString() {
return `(${this.x}, ${this.y})`;
,
};
}.equal(
assertString(point), '(7, 4)'); // (A)
.x
と .y
は基本レベルのプロパティです。これらは、点を使った計算の問題を解決するために使用されます。 .toString()
はメタレベルのプロパティです。問題領域とは関係ありません。
標準のECMAScriptメソッド .toJSON()
const point = {
x: 7,
y: 4,
toJSON() {
return [this.x, this.y];
,
};
}.equal(
assertJSON.stringify(point), '[7,4]');
.x
と .y
は基本レベルのプロパティであり、.toJSON()
はメタレベルのプロパティです。
プログラムの基本レベルとメタレベルは独立している必要があります。基本レベルのプロパティキーは、メタレベルのプロパティキーと競合してはなりません。
名前(文字列)をプロパティキーとして使用する場合、2つの課題に直面します。
言語が最初に作成されるとき、任意のメタレベル名を使用できます。基本レベルのコードは、それらの名前を避ける必要があります。しかし、後で多くの基本レベルのコードがすでに存在する場合、メタレベルの名前を自由に選択できなくなります。
基本レベルとメタレベルを区別するための命名規則を導入できます。たとえば、Pythonはメタレベルの名前を2つのアンダースコアで囲みます。__init__
、__iter__
、__hash__
など。ただし、言語のメタレベル名とライブラリのメタレベル名は、依然として同じ名前空間に存在し、衝突する可能性があります。
これらは、後者がJavaScriptの問題であった2つの例です。
2018年5月、配列メソッド .flatten()
は、前者の名前がすでにライブラリで使用されていたため、.flat()
に名前変更する必要がありました(ソース)。
2020年11月、配列メソッド .item()
は、前者の名前がすでにライブラリで使用されていたため、.at()
に名前変更する必要がありました(ソース)。
プロパティキーとして使用されるシンボルは、ここで役立ちます。各シンボルは一意であり、シンボルキーは他の文字列キーまたはシンボルキーと競合することはありません。
例として、特別なメソッドを実装しているオブジェクトを異なる方法で扱うライブラリを作成しているとします。これは、このようなメソッドのプロパティキーを定義し、オブジェクトに対して実装する方法です。
const specialMethod = Symbol('specialMethod');
const obj = {
_id: 'kf12oi',
// (A)
[specialMethod]() { return this._id;
};
}.equal(obj[specialMethod](), 'kf12oi'); assert
行Aの大括弧を使用すると、メソッドがキー specialMethod
を持つ必要があることを指定できます。詳細は、§28.7.2「オブジェクトリテラルの計算されたキー」で説明しています。
ECMAScript内で特別な役割を果たすシンボルは、*公開されているシンボル*と呼ばれます。例としては、
Symbol.iterator
: オブジェクトを*反復可能*にします。イテレータを返すメソッドのキーです。このトピックの詳細については、§30「同期反復」を参照してください。
Symbol.hasInstance
: instanceof
の動作をカスタマイズします。オブジェクトがこのキーを持つメソッドを実装している場合、その演算子の右側に使用できます。たとえば、
const PrimitiveNull = {
Symbol.hasInstance](x) {
[return x === null;
};
}.equal(null instanceof PrimitiveNull, true); assert
Symbol.toStringTag
: デフォルトの .toString()
メソッドに影響します。
> String({})'[object Object]'
> String({ [Symbol.toStringTag]: 'is no money' })'[object is no money]'
注: 通常は .toString()
をオーバーライドする方が適切です。
演習: 公開されているシンボル
Symbol.toStringTag
: exercises/symbols/to_string_tag_test.mjs
Symbol.hasInstance
: exercises/symbols/has_instance_test.mjs
シンボル sym
を別のプリミティブ型に変換するとどうなりますか?表 15 に答えがあります。
変換先 | 明示的な変換 | 型強制(暗黙的な変換) |
---|---|---|
真偽値 | Boolean(sym) → OK |
!sym → OK |
数値 | Number(sym) → TypeError |
sym*2 → TypeError |
文字列 | String(sym) → OK |
''+sym → TypeError |
sym.toString() → OK |
`${sym}` `→` `TypeError` |
シンボルに関する重要な落とし穴の1つは、シンボルを他の型に変換しようとすると、例外がスローされる頻度が高いことです。その背後にある考え方は何でしょうか?第一に、数値への変換は意味がなく、警告が表示されるべきです。第二に、シンボルを文字列に変換することは、診断出力に役立ちます。しかし、シンボルを誤って文字列(別の種類のプロパティキー)に変換することについても警告することも理にかなっています。
const obj = {};
const sym = Symbol();
.throws(
assert=> { obj['__'+sym+'__'] = true },
() message: 'Cannot convert a Symbol value to a string' }); {
欠点は、例外によってシンボルの扱いが複雑になることです。プラス演算子を使用して文字列を組み立てるときは、シンボルを明示的に変換する必要があります。
> const mySymbol = Symbol('mySymbol');
> 'Symbol I used: ' + mySymbolTypeError: Cannot convert a Symbol value to a string
> 'Symbol I used: ' + String(mySymbol)'Symbol I used: Symbol(mySymbol)'
クイズ
クイズアプリを参照してください。