well-known Symbolとは| 海外記事「Detailed overview of well-known symbols」の和訳
SymbolはECMAScript2015の新しいプリミティブ型です。それはユニークな識別子を作ることができます
[code language="javascript"]
let uniqueKey = Symbol('SymbolName')
[/code]
Symbolはオブジェクトの中のプロパティのkeyとして使うことが可能です。
(上のように使ったら)JavaScriptは「well-konwn symbols」として公開されたsymbolのリストを扱えます。
「Well-known symbols」はビルドインJavaScriptのアルゴリズムとして使われています。
例えば
Symbol.iterator はarrayやstringの要素を イテレートすること、もしくはあなた自身が定義したイテレーター関数などに利用されています。
それらの特別なSymbol等(ら)は重要です
なぜならそれらはオブクトのシステムプロパティで独自の振る舞いを定義できるからです。
それらをJSの中で使えるのです。
「唯一」として存在し、文字列イテレーターの代わりにkeyとしてシンボルを使うことは
新しいオブジェクトに新しい機能を追加することを簡単に可能にします
この記事ではwell-known Symbolのリストを通して、それらのコード中での快適な使い方を説明します
多くの場合単純化のために well-known Symbol.<name> は @@<name>形式に省略されています。
例えば
Symbol.iterator は @@iterator
Symbol.toPrimitiveは @@toPrimitive
「オブジェクトは @@iteratorメソッドを持つこと」を可能にすると言うことができます。
そのことは、オブジェクトはSymbol.iteratorと名付けられた、関数を所有するプロパティを持っていることを示しています。
↓
{ [Symbol.iterator]: function(){...} }
.
目次
- Symbolを短く紹介
- オブジェクトイテレータブルを作る@@iterator
- instanceofをカスタマイズする@@hasInstance
- オブジェクトをプリミティブに変換する@@toPrimitive
- オブジェクトのデフォルト記述を作成する@@toStringTag
- 派生されたオブジェクトを作成する@@species
- オブジェクトのような正規表現を作成 @@match, @@replace, @@search, @@split
- 配列要素に対してのオブジェクトをフラットにする@@isConcatSpreadable
- withの中のアクセス可能にするプロパティに対しての@@unscopables
- 終わりに
1.Symbolを短く紹介
Symbolは数値型、真偽値、文字列型のような唯一かつ不変なプリミティブ型です
symbolを作成するために、名前を引数とするオプションでSymbol関数を実行します。
[code language="javascript"]
let mySymbol = Symbol();
let namedSymbol = Symbol('myName');
typeof mySymbol;
//'symbol'
typeof namedSymbol
//'symbol'
[/code]
mySymbolとnamedSymbolは symbolプリミティブです。 namedSymbolは 'myName'ネームとの結びつきを持っていて、
それはデバッキングに対してよく使われます。
いつでもSymbol()が実行されることは重要で、
新しく且つユニークなsymbolが作られます。
2つのsymbolはたとえそれらが同じ名前を持っていてもユニークです。
[code language="javascript"]
let first = Symbol();
let second = Symbol();
first === second;
//false
let firstNamed = Symbol('Lorem');
let secondNamed = Symbol('Lorem');
firstNamed === secondNamed;
//false
[/code]
ユニークなシンボルで作られた first と second は違うものです。
firstNamed と secondNamed は 'Lorem' という同じ名前を持ちますが、違うものです
Symbols は オブジェクトのプロパティに対してkeyになることができます。
オブジェクトリテラル それか クラス定義の中で、
コンピューティッドプロパティネーム構文の [symbol]は使用は不可欠です。
[code language="javascript"]
let stringSymbol = Symbol('String');
let myObject = {
number: 1,
[stringSymbol]: 'Hello World'
};
myObject[stringSymbol];//'Hello World'
Object.getOwnPropertyNames(myObject); //['number']
Object.getOwnPropertySymbols(myObject);//['Symbol(String)']
[/code]
リテラルからmyObjectを定義している際、コンピューティッド構文はsymbol[stringSymbol]からのプロパティkeyを設定するために使われます。
シンボルで定義されたプロパティはObject.keys()やObject.getOwnPropertyNames()関数を使ってアクセスできません。
それらにアクセスするためには特別な関数であるObject.getOwnPropertySymbols()を呼びます。
keyとしてシンボルを使うことは重要な観点です。
特別なSymbol(well-known symbols)はイテレーションやプリミティブなオブジェクト、文字変換等のようなカスタムオブジェクトを定義することを可能にします。
well-known symbolsは列挙不可、書き換え不可、再設定不可 を可能にした Symbol関数オブジェクトのプロパティです
単純、Symbol.iteratorやSymbol.hasInstance等、それらを得るためのSymbol関数オブジェクト上のプロパティアクセサーを使います
このようなwell-known symbolsのリストを得ることができます。
[code language="javascript"]
Object.getOwnPropertyNames(Symbol);
//["hasInstance", "isConcatSpreadable", "iterator", "toPrimitive", "toStringTag", "unscopables","match", "replace", "search", "split", "species", ...];
typeof Symbol.iterator;
//'symbol'
[/code]
Object.getOwnPropertiesNames(Symbol) は Symbol関数オブジェクトの所有されたプロパティを返し、
well-known symbolsのリストを含んでいます。
Symbol.iteratorの進む「型」は'symbol'です。
オブジェクトイテレータブルを作る@@iterator
Symbol.iteratorは多分より知られているsymbolです。
それはオブジェクトがfor...of文やスピリードオペレータによってどのように使われ、イテレートされるべきかを定義することを可能にします
多くのstrings、arrays、maps、sets、などのビルドイン型はイテレータブルで、@@iteratorメソッドを持ちます。
[code language="javascript"]
let myString = 'Hola'
typeof myString[Symbol.iterator];// 'function'
for(let char of myString) {
console.log(char); //logs on each iterator 'H', 'o', 'I', 'a'
}
[...myString];// ['H', 'o', 'I', 'a']
[/code]
文字列のプリミティブ型のmyString はSymbol.iteratorプロパティを持ちます。
プロパティは文字列のキャラクターをイテレートすることに使うメソッドを所有します。
Symbol.iteratore という名前のメソッドを定義するオブジェクトはイテラブルプロトコルに準拠しています。
メソッドは イテレータープロトコルに準拠したオブジェクトを返す必要があります。
イテレータプロトコルオブジェクトは{value: , done: }を返すnext()メソッドを持つ必要があります。
カスタムイテレータの定義の仕方を理解しましょう。
下の例はmyMethods というイテラブルオブジェクトを作成、myMethodsはメソッドの所有を可能にします。
[code language="javascript"]
function methodsIterator(){
let index = 0;
let methods = Object.keys(this).filter((key) = > {
return typeof this[key] === 'function';
}).map(key => this[key]);
return {
next: ()=> ({//Conform to Iteretor protocol
done: index >= methods.length,
value: methods[index++]
})
};
}
let myMethods = {
toString: function(){
return '[object myMethods]';
},
sumNumbers: function(a, b) {
return a + b;
},
numbers: [1,5,6],
[Symbol.iterator]: methodsIterator //Comform to Iteratable Protocol
};
for ( let method of myMethods) {
console.log(method); //logs methods `toString` and `sumNumbers`
}
[/code]
methodsIterator() は イテレーターオブジェクト{next : function(){...}}を返す関数です。
myMethodsの中でオブジェクトプロパティはkeyとしてSymbol.iterator、値としてmethodsIteratorがセットアップされます
これはmyMethods をイテラブルにさせ、
for...ofループの中のsumNumbers()とtoString()らのオブジェクトが所有するメソッドを渡すことができます
それに加えて、[...myMethods]かArray.from(myMethods)を呼ぶことによってそれ等のメソッドを得ることができます。
@@iterator プロパティ は ジェネレーター関数も受け取り、そのジェネレーター関数を値にさせることもできます
@@iteratorメソッドでFibonacci シーケンスを生成する class Fibonacci を作ってみましょう
[code language="javascript"]
class Fibonacci {
constructor(n) {
this.n = n;
}
*[Symbol.iterator]() {
let a = 0, b = 1, index = 0;
while (index < this.n) { index++; let current = a; a = b; b = current + a; yield current; } } } let sequence = new Fibonacci(6); let numbers = [...sequence]; numbers; // => [0, 1, 1, 2, 3, 5]
[/code]
*[Symbol.iterator](){...} はジェネレーター関数はクラスメソッドということを明らかにします。
Fibonacciクラスのインスタンスはイテラブルプロトコルに準拠します。
そしてシーケンスインスタンスは[...sequence]スプレッドオペレーターで使われます。
スプレッドオペレーターは生成された数値からの配列を作成するために@@iterator メソッドを呼びます。
なので 結果は最初のFibonacci 数値の5の配列です。
もしプリミティブ型かオブジェクトが@@iteratorメソッドを持つなら、それらは下のコンストラクタ内で適応することができます。
・for...ofループの中での要素を超えイテレートする
・スプレッドオペレータ[...iterableObject]を使って要素の配列を作成する
・Array.from(iterableObject)を使って要素の配列を作成する
・他のジェネレーターへの代理する為のyield*式の中で
・Map(iterableObject)、WeakMap(iterableObject)、Set(iterableObject)、WeakSet(iterableObject)に対してのコンストラクタの中で
・静的メソッドPromise.all(iterableObject)、Promise.race(iterableObject)であるPromiseの中で
instanceofをカスタマイズする@@hasInstance
デフォルトでは obj instanceof Constructor オペレーターはConstructor.prototypeオブジェクトを含むObjectのprototypeのチェーンかどうか検証する
例題をみていきましょう
[code language="javascript"]
function Constructor(){
//constructor code
}
let obj = new Constructor();
let objProto = Object.getPrototypeOf(obj);
objProto === Constructor.prototype // true
obj instanceof Construcotr;//true
obj instanceof Object;//true
[/code]
obj instanceof Constructor はtrueかを評価します。なぜならobjのprototypeはConstructor.prototypeと同等だからです。
instanceof 検証はobjのprototypeチェーンも検証し、したがって、
obj instanceof Object は trueです。
多くのアプリケーションはprototypeに対処し、instanceの検証を必要としません
幸い、カスタマイズのinstanceof 評価を呼ぶことができる@@hasInstancseを定義できます。
obj instance Type はType[Symbol.hasInstance](obj)と同等です。
例えば、もしobjectかプリミティブがイテラブルの場合の検証をしましょう。
[code language="javascript"]
class Iterable {
static [Symbol.hasInstance](obj) {
return typeof obj[Symbol.iterator] === 'function';
}
}
let array = [1, 5, 5];
let string = 'Welcome';
let number = 15;
array instanceof Iterable; // => true
string instanceof Iterable; // => true
number instanceof Iterable; // => false
[/code]
イテラブルは@@hasInstance静的メソッドを含むクラスです。
このメソッドは供給されたobjパラメータがイテラブルか検証します
後のイテラブルかは違うタイプの値か検証に使われます
配列と文字列は はイテラブルで数値はそうではありません
個人的に単にinstanceof と constructorで@@hasInstanceを使うことは
isIterable(array)を呼ぶより
嬉しいです。
array instanceof Iterable は配列がイテラブルプロトコルに準拠しているかの検証されることとしてオススメします。
オブジェクトをプリミティブに変換する@@toPrimitive
プロパティの値記述としてのSymbol.toPrimitive 使用はオブジェクトからプリミティブへの変換する関数です。
@@toPrimitive メソッドは1つのnumberかstringかデフォルトを取るパラメーター[ヒント]を持ちます。
ヒントパラメーターは返すべきプリミティブの型の提案を示します
例えば 配列インスタンスを@@toPrimitiveメソッドで改善しましょう。
[code language="javascript"]
function arrayToPrimitive(hint) {
if (hint === 'number') {
return this.reduce((sum, num) => sum + num);
} else if (hint === 'string') {
return `[${this.join(', ')}]`;
} else {
// hint is default
return this.toString();
}
}
let array = [1, 5, 3];
array[Symbol.toPrimitive] = arrayToPrimitive;
// array to number. hint is 'number'
+ array; // => 9
// array to string. hint is 'string'
`array is ${array}`; // => 'array is [1, 5, 3]'
// array to default. hint is 'default'
'array elements: ' + array; // => 'array elements: 1,5,3'
[/code]
arrayToPrimitive(hint) は配列をhintから独立しているプリミティブへ変換する関数です
その割り当てである array[Symbol.toPrimitive] = arrayToPrimitive は新しい変換メソッドを使えるように配列を作ります。
+array は@@toPrimitiveメソッドを'number'で呼びます。
配列は数値に変換され、9の要素配列の合計です。
プリミティブ変換は'[1, 5, 3]'
最後の 'array elements: ' + arry は変換用のhintデフォルトを使います。
この場合 arrayは'1,5,3'に評価されます。
@@toPrimitive メソッドは オブジェクトがプリミティブ型と相互作用するとき使われます。
・object == primitive 同等操作の中で
・追加、連結操作 object + primitive
・減算操作の中で object - primitive
・オブジェクトがプリミティブへ強制されたときの違う状況で String(object), Number(objet)
オブジェクトのデフォルト記述を作成する@@toStringTag
プロパティを示すためにSymbol.toStringTagを使います。プロパティの値はオブジェクト型が記述された文字列です。
@@toStringTag メソッドは Object.prototype.toString()として使われています。
Object.prototype.toString()の仕様書は多くのJavaScript型はデフォルトとしてタグを持っていることを示します。
[code language="javascript"]
let toString = Object.prototype.toString;
toString.call(undefined); // => '[object Undefined]'
toString.call(null); // => '[object Null]'
toString.call([1, 4]); // => '[object Array]'
toString.call('Hello'); // => '[object String]'
toString.call(15); // => '[object Number]'
toString.call(true); // => '[object Boolean]'
// etc for Function, Arguments, Error, Date, RegExp
toString.call({}); // => '[object Object]'
[/code]
これらの型はプロパティSymbol.toStringTagを持っていません。
しかしObject.prototype.toString()アルゴリズムはそれらとは分けて評価します。
多くの他のJavaScript型はSymbol、ジェネレーター関数、maps、promises、など、で@@toStringTagプロパティを定義します。
ちょっと見てみましょう。
[code language="javascript"]
let toString = Object.prototype.toString;
let noop = function() {};
Symbol.iterator[Symbol.toStringTag]; // => 'Symbol'
(function* () {})[Symbol.toStringTag]; // => 'GeneratorFunction'
new Map()[Symbol.toStringTag]; // => 'Map'
new Promise(noop)[Symbol.toStringTag]; // => 'Promise'
toString.call(Symbol.iterator); // => '[object Symbol]'
toString.call(function* () {}); // => '[object GeneratorFunction]'
toString.call(new Map()); // => '[object Map]'
toString.call(new Promise(noop)); // => '[object Promise]'
[/code]
上のサンプルでみるように、
多くのJavaScript型はそれらが所有する@@toStringTagプロパティを定義します。
オブジェクトが型や@@toStringTagプロパティが提供されていない他のケースでは
単に'Object'としてタグされます。
もちろん@@toStringTagプロパティでカスタム定義できます。
[code language="javascript"]
let toString = Object.prototype.toString;
class SimpleClass {}
toString.call(new SimpleClass); // => '[object Object]'
class MyTypeClass {
constructor() {
this[Symbol.toStringTag] = 'MyType';
}
}
toString.call(new MyTypeClass); // => '[object MyType]'
"[object MyType]"
[/code]
new SimpleClass インスタンスは @@toStringTagが定義されていなかった。
Object.prototype.toString()はそれに対してデフォルトの'[object Object]'を返します。
MyTypeClassコンストラクタの中で、インスタンスはカスタムタグ'MyType'で設定されました。
そのようなObject.prototype.toString()クラスインスタンスに対してカスタム型記述は'[object MyType]'を返します。
@@toStringTagは下位互換の点で存在していることに注意してください。
その習慣はなやましいです。
あなたは他のオブジェクト型の線引きするための違う方法(instanceofかtypeof のような)を使うべきと思うだろう
派生オブジェクトを作成する@@species
Symbol.speciesプロパティの値は派生オブジェクト を作成するために使われたコンストラクタ関数です。
多くのJavaScriptコンストラクタはコンストラクタ自身と同等の@@speciesの値を持っています。
[code language="javascript"]
Array[Symbol.species] === Array; // => true
Map[Symbol.species] === Map; // => true
RegExp[Symbol.species] === RegExp; // => true
[/code]
最初に注意したいのは派生オブジェクトは元のオブジェクト上の操作後に作成されるものです。
例えばオリジナルな配列上の.map()メソッドを呼ぶと、派生オブジェクトを返します(配列結果をマッピングした)
大抵、派生オブジェクトはオリジナルのオブジェクトとして同じコンストラクタを持ち、もっていることを期待されます。
しかし時々カスタムコンストラクタを示すことが必要になります。
@@speciesプロパティはその助けとなります。
Arrayコンストラクタを子classのMyArrayとして使いやすいメソッドを加えるために拡張する際のシナリオを想定してください。
後でMyArray classインスタンスがmap()で使われるとき、
Arrayのインスタンスである必要があり、MyArrayはその子供ではない場合、それをします。
@@speciesプロパティアクセサを定義し派生したArrayコンストラクタオブジェクトを示します。
[code language="javascript"]
class MyArray extends Array {
isEmpty() {//拡張
return this.length === 0;
}
static get [Symbol.species]() {//プロパティアクセサを定義する
return Array;//派生オブジェクトを示す
}
}
let array = new MyArray(3, 5, 4);
array.isEmpty(); // => false
let odds = array.filter(item => item % 2 === 1);
odds instanceof Array; // => true
odds instanceof MyArray; // => false //違うことを示す
[/code]
静的アクセさプロパティのMyArrayでstatic get [Symbol.species](){}が定義されています。
それは派生オブジェクトはArrayコンストラクタを持つべきだという指示です。
array.filter()メソッドはArrayを返し、配列要素を選別した後に
@@species アクセサプロパティは.map()、.concat()、.slice()、.splice()のようなArrayとTypeArrayメソッドで使われ、派生オブジェクトを返します。
それはmaps、正規表現オブジェクト、プロミスなどの拡張しているものに対して有効で、オリジナルコンストラクタをキープできるのです。
オブジェクトのような正規表現を作成 @@match, @@replace, @@search, @@split
JavaScriptのString プロトタイプは 正規表現オブジェクトを受け入れる4つのメソッドを持っています。
・String.prototype.match(regExp)
・String.prototype.replace(regExp, newSubstr)
・String.prototype.search(regExp)
・String.prototype.split(regExp, limit)
ECMAScript 2015は それらの4つのメソッドは正規表現以外のタイプをプロパティの値とされた結果の関数を「条件」を定義することで、受け入れることができます。
@@match, @@replace, @@search and @@split.
面白いことにそれらのメソッドが定義された正規表現prototypeはsymbolを使うことで定義できます
[code language="javascript"]
typeof RegExp.prototype[Symbol.match]; // => 'function'
typeof RegExp.prototype[Symbol.replace]; // => 'function'
typeof RegExp.prototype[Symbol.search]; // => 'function'
typeof RegExp.prototype[Symbol.split]; // => 'function'
[/code]
カスタムパターンclassを作成してみましょう。
下の例はRegExpの代わりに使うことができる単純なclass定義です。
[code language="javascript"]
class Expression {
constructor(pattern) {
this.pattern = pattern;
}
[Symbol.match](str) {
return str.includes(this.pattern);
}
[Symbol.replace](str, replace) {
return str.split(this.pattern).join(replace);
}
[Symbol.search](str) {
return str.indexOf(this.pattern);
}
[Symbol.split](str) {
return str.split(this.pattern);
}
}
let sunExp = new Expression('sun');
'sunny day'.match(sunExp); // => true
'rainy day'.match(sunExp); // => false
'sunny day'.replace(sunExp, 'rainy'); // => 'rainy day'
"It's sunny".search(sunExp); // => 5
"daysunnight".split(sunExp); // => ['day', 'night']
[/code]
Expression classは@@match, @@replace, @@search and @@split. を定義しています。
後のsunExp インスタンスは 返すstring メソッドで使われて、だいたい擬似的な正規表現です。
配列要素に対してのオブジェクトをフラットにする@@isConcatSpreadable
-------------------------------------------------
以降はただいま翻訳中。。
長いな。。あと3つか。。