JavaScriptでSymbolデータ型を使用する機能

文字プリミティブは、JavaScriptにいくつかの価値ある機能をもたらしたES6標準の革新の1つです。 Symbolデータ型で表されるシンボルは、オブジェクトプロパティの識別子として使用する場合に特に便利です。 そのようなアプリケーションのシナリオに関連して、できること、できないことについて疑問が生じます。







本日翻訳を公開する資料では、JavaScriptのSymbolデータ型について説明します。 まず、シンボルを処理するためにナビゲートする必要があるJavaScript機能の一部の概要から始めます。



予備情報



JavaScriptには、本質的に2種類の値があります。 最初のタイプ-プリミティブ値、2番目-オブジェクト(関数も含まれます)。 プリミティブ値には、数値(整数から浮動小数点数、 Infinity



NaN



値まですべてを含む)などの単純なデータ型、論理値、文字列、 undefined



およびnull



値が含まれます。 typeof null === 'object'



true



生成することを確認する間、 null



はプリミティブな値であることに注意してください。



プリミティブ値は不変です。 変更することはできません。 もちろん、プリミティブ値を格納する変数に何か新しいものを書くことができます。 たとえば、ここでは、新しい値が変数x



書き込まれます。



 let x = 1; x++;
      
      





しかし、同時に、原始的な数値1



の変化(変化)はありません。



Cなどの一部の言語では、関数の引数を参照および値で渡すという概念があります。 JavaScriptにも同様のものがあります。 データの処理がどのように編成されるかは、そのタイプによって異なります。 特定の変数で表されるプリミティブ値が関数に渡され、この関数で変更された場合、元の変数に格納されている値は変更されません。 ただし、変数で表されるオブジェクト値を関数に渡し、それを変更すると、この変数に格納されているものも変更されます。



次の例を考えてみましょう。



 function primitiveMutator(val) { val = val + 1; } let x = 1; primitiveMutator(x); console.log(x); // 1 function objectMutator(val) { val.prop = val.prop + 1; } let obj = { prop: 1 }; objectMutator(obj); console.log(obj.prop); // 2
      
      





プリミティブ値(それ自体とは異なる神秘的なNaN



除く)は、常に自分自身のように見える他のプリミティブ値と等しくなります。 例:



 const first = "abc" + "def"; const second = "ab" + "cd" + "ef"; console.log(first === second); // true
      
      





ただし、外側が同じように見えるオブジェクト値の構築は、エンティティが取得されるという事実につながりません。比較すると、それらの相互の同等性が明らかになります。 これは次の方法で確認できます。



 const obj1 = { name: "Intrinsic" }; const obj2 = { name: "Intrinsic" }; console.log(obj1 === obj2); // false //     .name   : console.log(obj1.name === obj2.name); // true
      
      





オブジェクトはJavaScriptで基本的な役割を果たします。 それらは文字通りどこでも使用されています。 たとえば、キー/値コレクションの形で使用されることがよくあります。 しかし、 Symbol



データ型の出現前は、文字列のみがオブジェクトキーとして使用できました。 これは、コレクション形式のオブジェクトの使用に関する重大な制限でした。 オブジェクトキーとして文字列以外の値を割り当てようとしたときに、この値が文字列にキャストされました。 これは次の方法で確認できます。



 const obj = {}; obj.foo = 'foo'; obj['bar'] = 'bar'; obj[2] = 2; obj[{}] = 'someobj'; console.log(obj); // { '2': 2, foo: 'foo', bar: 'bar',    '[object Object]': 'someobj' }
      
      





ちなみに、これは文字のトピックから少し離れていますが、キーが文字列ではない状況でキー/値のデータストアを使用できるようにMap



データ構造が作成されたことに注意したいと思います。



シンボルとは何ですか?



JavaScriptのプリミティブ値の機能を理解したので、いよいよキャラクターについて話し始める準備ができました。 シンボルは、一意のプリミティブな意味です。 この位置からシンボルに近づくと、シンボルのいくつかのインスタンスを作成すると異なる値が作成されるため、この点でのシンボルはオブジェクトに似ていることに気付くでしょう。 しかし、シンボルはさらに、不変のプリミティブ値です。 以下はキャラクターの操作例です:



 const s1 = Symbol(); const s2 = Symbol(); console.log(s1 === s2); // false
      
      





文字のインスタンスを作成するとき、オプションの最初の文字列引数を使用できます。 この引数は、デバッグで使用するためのシンボルの説明です。 この値は、シンボル自体には影響しません。



 const s1 = Symbol('debug'); const str = 'debug'; const s2 = Symbol('xxyy'); console.log(s1 === str); // false console.log(s1 === s2); // false console.log(s1); // Symbol(debug)
      
      





オブジェクトのプロパティへのキーとしてのシンボル



シンボルは、オブジェクトのプロパティキーとして使用できます。 これは非常に重要です。 これらをそのまま使用する例を次に示します。



 const obj = {}; const sym = Symbol(); obj[sym] = 'foo'; obj.bar = 'bar'; console.log(obj); // { bar: 'bar' } console.log(sym in obj); // true console.log(obj[sym]); // foo console.log(Object.keys(obj)); // ['bar']
      
      





Object.keys()



メソッドがObject.keys()



とき、文字で指定されたキーは返されないことに注意してください。 JSで文字が出現する前に記述されたコードは文字について何も知らないため、文字で表されるオブジェクトのキーに関する情報は、古代のObject.keys()



メソッドによって返されるべきではありません。



一見、上記のキャラクターの機能を使用して、JSオブジェクトのプライベートプロパティを作成できるように思えるかもしれません。 他の多くのプログラミング言語では、クラスを使用してオブジェクトの隠しプロパティを作成できます。 この機能の欠如は、JavaScriptの欠点の1つと長い間考えられてきました。



残念ながら、オブジェクトで機能するコードは、文字列キーに自由にアクセスできます。 さらに、オブジェクトを操作するコードが対応する文字にアクセスできない場合でも、コードは文字で指定されたキーにアクセスできます。 たとえば、 Reflect.ownKeys()



メソッドを使用すると、オブジェクトのすべてのキーのリストを取得できます。これらは、文字列であるキーと文字であるキーの両方です。



 function tryToAddPrivate(o) { o[Symbol('Pseudo Private')] = 42; } const obj = { prop: 'hello' }; tryToAddPrivate(obj); console.log(Reflect.ownKeys(obj));       // [ 'prop', Symbol(Pseudo Private) ] console.log(obj[Reflect.ownKeys(obj)[1]]); // 42
      
      





クラスにプライベートプロパティを使用する機能を装備する作業が現在進行中であることに注意してください。 この機能はプライベートフィールドと呼ばれます。 確かに、前もって準備されたクラスに基づいて作成されたオブジェクトのみを参照して、すべてのオブジェクトに影響を与えるわけではありません。 プライベートフィールドのサポートは、Chromeブラウザバージョン72以前で既に利用可能です。



オブジェクトプロパティ名の衝突を防ぐ



もちろん、シンボルはJavaScriptにオブジェクトのプライベートプロパティを作成する機能を追加するものではありませんが、他の理由で言語の価値ある革新です。 つまり、特定のライブラリがそれらの外部に記述されたオブジェクトにプロパティを追加する必要があり、同時にオブジェクトのプロパティの名前の衝突を恐れない場合に役立ちます。



2つの異なるライブラリがオブジェクトにメタデータを追加する例を考えてみましょう。 両方のライブラリがオブジェクトにいくつかの識別子を装備する必要がある可能性があります。 このようなプロパティの名前に2文字のid



文字列のようなものを単純に使用すると、一方のライブラリが他方のライブラリで指定されたプロパティを上書きする場合があります。



 function lib1tag(obj) { obj.id = 42; } function lib2tag(obj) { obj.id = 369; }
      
      





この例でシンボルを使用すると、各ライブラリは初期化時に必要なシンボルを生成できます。 これらのシンボルを使用して、プロパティをオブジェクトに割り当て、これらのプロパティにアクセスできます。



 const library1property = Symbol('lib1'); function lib1tag(obj) { obj[library1property] = 42; } const library2property = Symbol('lib2'); function lib2tag(obj) { obj[library2property] = 369; }
      
      





このようなシナリオを見ると、JavaScriptでの文字の表示から利益を得ることができます。



ただし、オブジェクトのプロパティ名、ランダムな文字列、またはライブラリの名前などの複雑な構造を持つ文字列の名前のライブラリの使用に関して疑問があるかもしれません。 同様の文字列は、ライブラリで使用される識別子の名前空間のようなものを形成できます。 たとえば、次のようになります。



 const library1property = uuid(); //       function lib1tag(obj) { obj[library1property] = 42; } const library2property = 'LIB2-NAMESPACE-id'; //     function lib2tag(obj) { obj[library2property] = 369; }
      
      





一般に、そうすることができます。 実際、同様のアプローチは、シンボルを使用するときに起こるものと非常に似ています。 そして、ランダムな識別子または名前空間を使用して、偶然に同じプロパティ名を生成しないライブラリがいくつかある場合、名前に問題はありません。



賢明な読者は、オブジェクトプロパティの命名を検討中の2つのアプローチは完全に同等ではないと言うでしょう。 ランダムに生成される、または名前空間を使用して生成されるプロパティ名には欠点があります。特に、コードがオブジェクトのキーを検索またはシリアル化する場合、対応するキーを見つけるのは非常に簡単です。 次の例を考えてみましょう。



 const library2property = 'LIB2-NAMESPACE-id'; //    function lib2tag(obj) { obj[library2property] = 369; } const user = { name: 'Thomas Hunter II', age: 32 }; lib2tag(user); JSON.stringify(user); // '{"name":"Thomas Hunter II","age":32,"LIB2-NAMESPACE-id":369}'
      
      





この状況でキー名にシンボルが使用された場合、オブジェクトのJSON表現にはシンボル値が含まれません。 これはなぜですか? 事実は、新しいデータ型がJavaScriptに出現したという事実は、JSON仕様に変更が加えられたことを意味するものではありません。 JSONは、文字列プロパティキーのみをオブジェクトプロパティキーとしてサポートします。 オブジェクトをシリアル化する場合、特別な方法で文字を表現する試みは行われません。



Object.defineProperty()



を使用すると、オブジェクトのJSON表現にプロパティ名を取得するという問題を解決できます。



 const library2property = uuid(); //   function lib2tag(obj) { Object.defineProperty(obj, library2property, {   enumerable: false,   value: 369 }); } const user = { name: 'Thomas Hunter II', age: 32 }; lib2tag(user); // '{"name":"Thomas Hunter II",  "age":32,"f468c902-26ed-4b2e-81d6-5775ae7eec5d":369}' console.log(JSON.stringify(user)); console.log(user[library2property]); // 369
      
      





enumerable



記述子false



設定することで「隠された」文字列キーは、文字で表されるキーとほぼ同じように動作します。 Object.keys()



呼び出された場合、これらは両方とも表示されず、 Object.keys()



を使用して両方を検出できます。 これは次のようなものです。



 const obj = {}; obj[Symbol()] = 1; Object.defineProperty(obj, 'foo', { enumberable: false, value: 2 }); console.log(Object.keys(obj)); // [] console.log(Reflect.ownKeys(obj)); // [ 'foo', Symbol() ] console.log(JSON.stringify(obj)); // {}
      
      





ここで、JSの他の手段を使用して、シンボルの可能性をほぼ再現したと言わざるを得ません。 特に、シンボルで表されるキーと秘密キーの両方は、オブジェクトのJSON表現に該当しません。 どちらもReflect.ownKeys()



メソッドを参照することで認識できます。 その結果、両方を真にプライベートと呼ぶことはできません。 いくつかのランダムな値またはライブラリの名前空間がキー名の生成に使用されると仮定する場合、これは名前の衝突のリスクを取り除くことを意味します。



ただし、シンボリック名を使用することと、他のメカニズムを使用して作成された名前を使用することとの間にわずかな違いがあります。 文字列は不変であり、文字は一意であることが保証されているため、文字列内の文字のすべての可能な組み合わせを通過した後、名前の衝突が発生する可能性が常にあります。 数学的な観点から、これは文字が文字列にはない貴重な機会を本当に与えてくれることを意味します。



Node.jsでは、オブジェクトを調べるときに(たとえば、 console.log()



を使用して)、 inspect



というオブジェクトメソッドinspect



検出されると、このメソッドを使用してオブジェクトの文字列表現を取得し、画面に表示します。 絶対に誰もこれを考慮に入れることができないことは容易に理解できます。したがって、システムのこのような動作は、オブジェクトの文字列表現の形成に関係しない問題を解決するために設計されたオブジェクトinspect



メソッドの呼び出しにつながる可能性があります。 この機能はNode.js 10では非推奨です。バージョン11では、同様の名前のメソッドは単に無視されます。 この機能を実装するrequire('util').inspect.custom



に、 require('util').inspect.custom



ます。 これは、 inspect



と呼ばれるオブジェクトメソッドを作成することにより、システムを不注意に混乱させることができないことを意味します。



私有財産の模倣



オブジェクトのプライベートプロパティをシミュレートするために使用できる興味深いアプローチを次に示します。 このアプローチには、もう1つの最新のJavaScript機能であるプロキシオブジェクトの使用が含まれます。 このようなオブジェクトは、プログラマがこれらのオブジェクトで実行されるアクションに介入できるようにする他のオブジェクトのラッパーとして機能します。



プロキシオブジェクトは、オブジェクトに対して実行されるアクションをインターセプトする多くの方法を提供します。 オブジェクトのキーの読み取り操作を制御する機能に興味があります。 ここでは、プロキシオブジェクトの詳細については説明しません。 興味のある方は、 この出版物をご覧ください。



プロキシを使用して、オブジェクトのどのプロパティが外部から見えるかを制御できます。 この場合、既知の2つのプロパティを非表示にするプロキシを作成します。 1つは文字列名_favColor



を持ち、2つ目はfavBook



変数に書き込まれた文字で表されます。



 let proxy; { const favBook = Symbol('fav book'); const obj = {   name: 'Thomas Hunter II',   age: 32,   _favColor: 'blue',   [favBook]: 'Metro 2033',   [Symbol('visible')]: 'foo' }; const handler = {   ownKeys: (target) => {     const reportedKeys = [];     const actualKeys = Reflect.ownKeys(target);     for (const key of actualKeys) {       if (key === favBook || key === '_favColor') {         continue;       }       reportedKeys.push(key);     }     return reportedKeys;   } }; proxy = new Proxy(obj, handler); } console.log(Object.keys(proxy)); // [ 'name', 'age' ] console.log(Reflect.ownKeys(proxy)); // [ 'name', 'age', Symbol(visible) ] console.log(Object.getOwnPropertyNames(proxy)); // [ 'name', 'age' ] console.log(Object.getOwnPropertySymbols(proxy)); // [Symbol(visible)] console.log(proxy._favColor); // 'blue
      
      





名前が文字列_favColor



表されるプロパティを扱うことは難しくありません。ソースコードを読むだけです。 動的キー(上で見たuuidキーなど)は、ブルートフォースと一致させることができます。 ただし、シンボル参照がないと、 proxy



オブジェクトからMetro 2033



値にアクセスできません。



Node.jsには、プロキシオブジェクトのプライバシーを侵害する機能が1つあります。 この機能は言語自体には存在しないため、ブラウザなどの他のJSランタイムには関係ありません。 実際、この機能を使用すると、プロキシオブジェクトにアクセスできる場合、プロキシオブジェクトの背後に隠されたオブジェクトにアクセスできます。 以下は、前のコードスニペットで示されたメカニズムをバイパスする機能を示す例です。



 const [originalObject] = process .binding('util') .getProxyDetails(proxy); const allKeys = Reflect.ownKeys(originalObject); console.log(allKeys[3]); // Symbol(fav book)
      
      





Node.jsの特定のインスタンスでこの機能が使用されないようにするには、グローバルReflect



オブジェクトまたはutil



プロセスのバインディングを変更する必要があります。 ただし、これは別のタスクです。 興味がある場合は、JavaScriptベースのAPIの保護に関するこの投稿をご覧ください。



まとめ



この記事では、 Symbol



データ型、それがJavaScript開発者に提供する機能、およびこれらの機能をシミュレートするために使用できる既存の言語メカニズムについて説明しました。



親愛なる読者! JavaScriptプロジェクトでシンボルを使用していますか?






All Articles