よく知られているシンボルの詳細な概要





良い一日、友達!



Symbolは、ECMAScript2015(ES6)で導入されたプリミティブデータタイプであり、一意の識別子を作成できます。constuniqueKey = Symbol( 'SymbolName')。



オブジェクトプロパティのキーとしてシンボルを使用できます。 JavaScriptが特別な方法で処理するシンボルは、既知のシンボルと呼ばます。これらの文字は、組み込みのJavaScriptアルゴリズムによって使用されます。たとえば、Symbol.iteratorは、配列、文​​字列の要素を反復処理するために使用されます。また、独自のイテレーター関数を定義するために使用することもできます。



これらのシンボルは、オブジェクトの動作を微調整できるため、重要な役割を果たします。



一意であるため、(文字列の代わりに)オブジェクトキーとしてシンボルを使用すると、オブジェクトに新しい機能を簡単に追加できます。同時に、文字列を使用するときに問題になる可能性があるキー間の衝突を心配する必要はありません(各文字は一意であるため)。



この記事では、よく知られているシンボルとその使用例に焦点を当てます。



簡単にするために、よく知られているSymbol。<name>シンボルの構文は@@ <name>形式です。たとえば、Symbol.iteratorは@@ iteratorとして表され、Symbol.toPrimitiveは@@ toPrimitiveとして表されます。



オブジェクトに@@ iteratorメソッドがあるとすると、オブジェクトには、関数{[Symbol.iterator]:function(){}}で表されるSymbol.iteratorというプロパティが含まれます。



1.シンボルの簡単な紹介



文字は、プリミティブタイプ(数値、文字列、ブール値など)であり、一意で不変(不変)です。



シンボルを作成するには、オプションの引数(シンボルの名前、より正確には説明)を指定してSymbol()関数を呼び出します。



const mySymbol = Symbol()
const namedSymbol = Symbol('myName')
typeof mySymbol // symbol
typeof namedSymbol // symbol


mySymbolとnamedSymbolはプリミティブシンボルです。namedSymbolの名前は「myName」で、通常はコードのデバッグに使用されます。



Symbol()を呼び出すたびに、新しい一意のシンボルが作成されます。2つの文字は、同じ名前であっても一意(または特殊)です。



const first = Symbol()
const second = Symbol()
first === second // false

const firstNamed = Symbol('Lorem')
const secondNamed = Symbol('Lorem')
firstNamed === secondNamed // false


シンボルはオブジェクトのキーにすることができます。これを行うには、オブジェクトリテラルまたはクラス定義で計算されたプロパティ構文([symbol])を使用します。



const strSymbol = Symbol('String')

const myObj = {
  num: 1,
  [strSymbol]: 'Hello World'
}

myObj[strSymbol] // Hello World
Object.getOwnPropertyNames(myObj) // ['num']
Object.getOwnPropertySymbols(myObj) // [Symbol(String)]


Object.keys()またはObject.getOwnPropertyNames()を使用してシンボルプロパティを取得することはできません。それらにアクセスするには、特別な関数Object.getOwnPropertySymbols()を使用する必要があります。



よく知られているシンボルをキーとして使用すると、オブジェクトの動作を変更できます。



よく知られているシンボルは、Symbolオブジェクトの列挙不可能、不変、および構成不可能なプロパティとして利用できます。それらを取得するには、ドット表記を使用します:Symbol.iterator、Symbol.hasInstanceなど。



よく知られているシンボルのリストを取得する方法は次のとおりです。



Object.getOwnPropertyNames(Symbol)
// ["hasInstance", "isConcatSpreadable", "iterator", "toPrimitive",
//  "toStringTag", "unscopables", "match", "replace", "search",
//  "split", "species", ...]

typeof Symbol.iterator // symbol


Object.getOwnPropertyNames(Symbol)は、既知のシンボルを含む、Symbolオブジェクトのネイティブプロパティのリストを返します。もちろん、Symbol.iteratorはsymbolタイプです。



2. @@ iterator。オブジェクトを反復可能(反復可能)にすることができます。



Symbol.iteratorは、おそらく最もよく知られているシンボルです。これにより、for-ofステートメントまたはspread演算子を使用して、オブジェクトをどのように反復するか(および、オブジェクトを反復するかどうか)を定義できます。



文字列、配列、マップ、セット、セットなどの多くの組み込み型は、@@ iteratorメソッドがあるため、デフォルトで反復可能です。



const myStr = 'Hi'
typeof myStr[Symbol.iterator] // function
for (const char of myStr) {
  console.log(char) //     :  'H',  'i'
}
[...myStr] // ['H', 'i']


変数myStrには、Symbol.iteratorプロパティを持つプリミティブ文字列が含まれています。このプロパティには、文字列内の文字を反復処理するために使用される関数が含まれています。



Symbol.iteratorメソッドが定義されているオブジェクトは、反復(iterator)プロトコルに準拠している必要があります。より正確には、このメソッドは、指定されたプロトコルに準拠するオブジェクトを返す必要があります。このようなオブジェクトには、{value:<iterator_value>、done:<boolean_finished_iterator>}を返すnext()メソッドが必要です。



次の例では、反復可能なmyMethodsオブジェクトを作成して、そのメソッドを反復処理します。



function methodsIterator() {
  let index = 0
  const methods = Object.keys(this)
    .filter(key => typeof this[key] === 'function')

    return {
      next: () => ({
        done: index === methods.length,
        value: methods[index++]
      })
    }
}

const myMethods = {
  toString: () => '[object myMethods]',
  sum: (a, b) => a + b,
  numbers: [1, 3, 5],
  [Symbol.iterator]: methodsIterator
}

for (const method of myMethods) {
  console.log(method) // toString, sum
}


methodsIterator()は、イテレーター{next:function(){}}を返す関数です。 myMethodsオブジェクトは、値methodsIteratorを使用して計算プロパティ[Symbol.iterator]を定義します。これにより、for-ofループを使用してオブジェクトを反復可能にします。オブジェクトメソッドは、[... myMethods]を使用して取得することもできます。このようなオブジェクトは、Array.from(myMethods)を使用して配列に変換できます。ジェネレーター関数



を使用すると、反復可能なオブジェクトの作成を簡略化できます。この関数は、反復プロトコルに準拠するGeneratorオブジェクト返します。 一連のFibonacci番号を生成する@@ iteratorメソッドを使用してFibonacciクラスを作成しましょう。







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
    }
  }
}

const sequence = new Fibonacci(6)
const numbers = [...sequence]
console.log(numbers) // [0, 1, 1, 2, 3, 5]


* [Symbol.iterator](){}は、クラスメソッド(ジェネレーター関数)を定義します。Fibonacciインスタンスは、ブルートフォースプロトコルに従います。Spread演算子は、@@ iteratorメソッドを呼び出して、数値の配列を作成します。



プリミティブタイプまたはオブジェクトに@@イテレーターが含まれている場合、次のシナリオで使用できます。



  • for-ofを使用して要素を反復処理する
  • スプレッド演算子を使用して要素の配列を作成する
  • Array.from(iterableObject)を使用して配列を作成する
  • 別のジェネレータに渡すyield *式で
  • コンストラクターでは、Map()、WeakMap()、Set()、WeakSet()
  • 静的メソッドでは、Promise.all()、Promise.race()など。


反復可能なオブジェクトの作成について詳しくは、こちらをご覧ください



3.instanceofを構成する@@ hasInstance



デフォルトでは、obj instanceof Constructorオペレーターは、objプロトタイプチェーンにConstructor.prototypeオブジェクトが含まれているかどうかを確認します。例を考えてみましょう:



function Constructor() {
  // ...
}
const obj = new Constructor()
const objProto = Object.getPrototypeOf(obj)

objProto === Constructor.prototype // true
obj instanceof Constructor // true
obj instanceof Object // true


objのプロトタイプはConstructor.prototypeであるため(コンストラクターを呼び出した結果)、objinstanceofConstructorはtrueを返します。instanceofは必要に応じてプロトタイプチェーンを参照するため、obj instanceofObjectもtrueを返します。



アプリケーションでより厳密なインスタンスチェックが必要になる場合があります。



幸い、instanceofの動作を変更する@@ hasInstanceメソッドを定義する機能があります。Typeのobjinstanceofは、Type [Symbol.hasInstance](obj)と同等です。



変数が反復可能かどうかを確認しましょう。



class Iterable {
  static [Symbol.hasInstance](obj) {
    return typeof obj[Symbol.iterator] === 'function'
  }
}

const arr = [1, 3, 5]
const str = 'Hi'
const num = 21
arr instanceof Iterable // true
str instanceof Iterable // true
num instanceof Iterable // false


Iterableクラスには、静的メソッド@@ hasInstanceが含まれています。このメソッドは、objが反復可能かどうかをチェックします。Symbol.iteratorプロパティが含まれているかどうか。arrとstrは反復可能ですが、numは反復可能ではありません。



4. @@ toPrimitiveは、オブジェクトをプリミティブに変換します



Symbol.toPrimitiveを使用して、値がオブジェクトからプリミティブへの変換関数であるプロパティを定義します。@@ toPrimitiveは、数値、文字列、またはデフォルトの1つのパラメーター、ヒントを取ります。ヒントは、戻り値のタイプを示します。



配列の変換を改善しましょう:



function arrayToPrimitive(hint) {
  if (hint === 'number') {
    return this.reduce((x, y) => x + y)
  } else if (hint === 'string') {
    return `[${this.join(', ')}]`
  } else {
    // hint    
    return this.toString()
  }
}

const array = [1, 3, 5]
array[Symbol.toPrimitive] = arrayToPrimitive

//    . hint  
+ array // 9
//    . hint  
`array is ${array}` // array is [1, 3, 5]
//   . hint   default
'array elements: ' + array // array elements: 1,3,5


arrayToPrimitive(ヒント)は、ヒント値に基づいて配列をプリミティブに変換する関数です。array [Symbol.toPrimitive]をarrayToPrimitiveに設定すると、配列は新しい変換メソッドを使用するように強制されます。+配列を実行すると、ヒント値がnumberの@@ toPrimitiveが呼び出されます。配列要素の合計が返されます。array is $ {array}はhint = stringで@@ toPrimitiveを呼び出します。配列は文字列 '[1、3、5]'に変換されます。最後に、 '配列要素:' +配列はヒント=デフォルトを使用して変換します。配列は「1,3,5」に変換されます。



@@ toPrimitiveメソッドは、オブジェクトをプリミティブタイプとして表すために使用されます。



  • 緩い(抽象的な)等式演算子を使用する場合:object ==プリミティブ
  • 追加/連結演算子を使用する場合:オブジェクト+プリミティブ
  • 減算演算子を使用する場合:オブジェクト-プリミティブ
  • さまざまな状況で、オブジェクトをプリミティブに変換します:文字列(オブジェクト)、数値(オブジェクト)など。


5. @@ toStringTagを使用して、標準のオブジェクト記述を作成します



Symbol.toStringTagを使用して、値がオブジェクトのタイプを説明する文字列であるプロパティを定義します。@@ toStringTagメソッドは、Object.prototype.toString()によって使用されます。



仕様は、多くのタイプのObject.prototype.toString()によって返されるデフォルト値を定義します:



const 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]
// Function, Arguments, Error, Date, RegExp  ..
toString.call({})        // [object Object]


Object.prototype.toString()アルゴリズムが特別な方法でそれらを評価するため、これらのタイプにはSymbol.toStringTagプロパティがありません。



問題のプロパティは、シンボル、ジェネレーター関数、カード、プロミスなどのタイプで定義されています。例を考えてみましょう。



const toString = Object.prototype.toString
const 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]


オブジェクトが標準タイプのグループではなく、@@ toStringTagプロパティが含まれていない場合、Objectが返されます。もちろん、これを変更することもできます。



const 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]


SimpleClassクラスのインスタンスには@@ toStringTagプロパティがないため、Object.prototype.toString()は[objectObject]を返します。MyTypeClassクラスのコンストラクターは、@@ toStringTagプロパティを値MyTypeのインスタンスに割り当てるため、Object.prototype.toString()は[objectMyType]を返します。



@@ toStringTagは、下位互換性のために導入されたことに注意してください。その使用は望ましくありません。オブジェクトのタイプを判別するには、instanceof(@@ hasInstanceと一緒に)またはtypeofを使用することをお勧めします。



6.派生オブジェクトを作成する@@種



Symbol.speciesを使用して、派生オブジェクトの作成に使用されるコンストラクター関数を値とするプロパティを定義します。



多くのコンストラクターの@@種の値は、コンストラクター自体です。



Array[Symbol.species] === Array // true
Map[Symbol.species] === Map // true
RegExp[Symbol.species] === RegExp // true


まず、派生オブジェクトは、元のオブジェクトに対して特定の操作を実行した後に返されるオブジェクトであることに注意してください。たとえば、map()を呼び出すと、派生オブジェクト(配列の要素を変換した結果)が返されます。



通常、派生オブジェクトは元のオブジェクトと同じコンストラクターを参照します。ただし、別のコンストラクター(おそらく標準クラスの1つ)を定義する必要がある場合があります。これは、@@種が役立つ場合があります。



いくつかの便利なメソッドを追加するために、MyArray子クラスでArrayコンストラクターを拡張するとします。その際、MyArrayインスタンスの派生オブジェクトのコンストラクターをArrayにする必要があります。これを行うには、計算されたプロパティ@@種を配列値で定義する必要があります。



class MyArray extends Array {
  isEmpty() {
    return this.length === 0
  }
  static get [Symbol.species]() {
    return Array
  }
}
const array = new MyArray(2, 3, 5)
array.isEmpty() // false
const odds = array.filter(item => item % 2 === 1)
odds instanceof Array // true
odds instanceof MyArray // false


MyArrayは、静的計算プロパティSymbol.speciesを定義します。派生オブジェクトのコンストラクターを配列コンストラクターにすることを指定します。後で配列の要素をフィルタリングするときに、array.filter()は配列を返します。



@@種計算プロパティは、派生オブジェクトを返すmap()、concat()、slice()、splice()などの配列および型付き配列メソッドによって使用されます。このプロパティを使用すると、元のコンストラクターを保持しながら、マップ、正規表現、またはPromiseを拡張するのに役立ちます。



7.オブジェクトの形式で正規式を作成します:@@一致、@@置換、@@検索、@@分割



文字列プロトタイプには、通常の式を引数として取る4つのメソッドが含まれています。



  • String.prototype.match(regExp)
  • String.prototype.replace(regExp、newSubstr)
  • String.prototype.search(regExp)
  • String.prototype.split(regExp、limit)


ES6では、対応する計算されたプロパティが定義されている限り、これらのメソッドが他のタイプを受け入れることができます:@@ match、@@ replace、@@ search、および@@ split。



不思議なことに、RegExpプロトタイプには、シンボルを使用して定義された、指定されたメソッドが含まれています。



typeof RegExp.prototype[Symbol.match]   // function
typeof RegExp.prototype[Symbol.replace] // function
typeof RegExp.prototype[Symbol.search]  // function
typeof RegExp.prototype[Symbol.split]   // function


次の例では、通常の式の代わりに使用できるクラスを定義しています。



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)
  }
}

const sunExp = new Expression('')

' '.match(sunExp) // true
' '.match(sunExp) // false
' day'.replace(sunExp, '') // ' '
'  '.search(sunExp) // 8
''.split(sunExp) // ['', '']


Expressionクラスは、@@ match、@@ replace、@@ search、および@@ splitメソッドを定義します。次に、このクラスのインスタンス-sunExpが、通常の式の代わりに適切なメソッドで使用されます。



8. @@ isConcatSp読み取り可能で、オブジェクトを配列に変換します



Symbol.isConcatSp読み取り可能は、Array.prototype.concat()メソッドを使用してオブジェクトを配列に変換できることを示すブール値です。



デフォルトでは、concat()メソッドは、配列を連結するときに、配列の要素を取得します(配列を構成要素に分解します)。



const letters = ['a', 'b']
const otherLetters = ['c', 'd']
otherLetters.concat('e', letters) // ['c', 'd', 'e', 'a', 'b']


2つの配列を連結するには、concat()メソッドに引数として文字を渡します。文字配列の要素は、連結の結果の一部になります:['c'、 'd'、 'e'、 'a'、 'b']。



配列が要素に分解されるのを防ぎ、配列をそのままユニオン結果の一部にするために、@@ isConcatSp読み取り可能プロパティをfalseに設定する必要があります。



const letters = ['a', 'b']
letters[Symbol.isConcatSpreadable] = false
const otherLetters = ['c', 'd']
otherLetters.concat('e', letters) // ['c', 'd', 'e', ['a', 'b']]


配列とは対照的に、concat()メソッドは配列のようなオブジェクトを要素に分解しません。この動作は、@@ isConcatSp読み取り可能で変更することもできます。



const letters = { 0: 'a', 1: 'b', length: 2 }
const otherLetters = ['c', 'd']
otherLetters.concat('e', letters)
// ['c', 'd', 'e', {0: 'a', 1: 'b', length: 2}]
letters[Symbol.isConcatSpreadable] = true
otherLetters.concat('e', letters) // ['c', 'd', 'e', 'a', 'b']


9.でプロパティにアクセスするための@@ unscopables



Symbol.unscopablesは、withステートメントを使用してスコープチェーンの先頭に追加されたオブジェクトから適切な名前が除外される計算プロパティです。@@ unscopablesプロパティの形式は次のとおりです:{propertyName:<boolean_exclude_binding>}。



ES6は、@@ unscopablesを配列に対してのみ定義します。これは、古いコードで同じ名前の変数を上書きできる新しいメソッドを非表示にするために行われます。



Array.prototype[Symbol.unscopables]
// { copyWithin: true, entries: true, fill: true,
//   find: true, findIndex: true, keys: true }
let numbers = [1, 3, 5]
with (numbers) {
  concat(7) // [1, 3, 5, 7]
  entries // ReferenceError: entries is not defined
}


このメソッドは@@ unscopablesプロパティに含まれていないため、with bodyのconcat()メソッドにアクセスできます。エントリ()メソッドはこのプロパティで指定され、trueに設定されているため、内部では使用できません。



@@ unscopablesは、withステートメントを使用したレガシーコードとの下位互換性のためにのみ導入されました(厳密モードでは非推奨および禁止)。



何か面白いものを見つけていただければ幸いです。清聴ありがとうございました。



All Articles