JavaScriptと突然変異の恐怖

突然変異は変化です。 形の変化または本質の変化。 変異するものは変更される可能性があります。 突然変異の性質をよりよく理解するために、映画「X-メン」のヒーローについて考えてください。 彼らは突然素晴らしい機会を得ることができました。 ただし、問題は、これらの可能性がいつ現れるかが正確にわからないことです。 あなたの同志が理由もなく青くなり、ウールで生い茂ったと想像してください。 怖いですよね? JavaScriptにも同じ問題があります。 コードが突然変異の影響を受ける場合、これは非常に予想外に、何かを変更したり壊したりできることを意味します。



JavaScriptオブジェクトと突然変異


JavaScriptオブジェクトにプロパティを追加できます。 オブジェクトのインスタンス化後にこれが行われると、オブジェクトは不可逆的に変更されます。 彼はX-Menのキャラクターの1人のように突然変異します。

次の例では、オブジェクトである定数eggは、 isBrokenプロパティが追加された後にisBrokenます。 そのようなオブジェクト( eggような)を変更可能(つまり、変更、変更の能力がある)と呼びます。

 const egg = { name: "Humpty Dumpty" }; egg.isBroken = false; console.log(egg); // { //   name: "Humpty Dumpty", //   isBroken: false // } 

JavaScriptでは、突然変異は非常に一般的です。 あなたは文字通り常にどこでもそれらに直面することができます。

突然変異の危険性について


eggオブジェクトが書き込まれるnewEggという名前の定数を作成するとします。 次に、 newEgg nameプロパティを変更する必要がありnewEgg

 const egg = { name: "Humpty Dumpty" }; const newEgg = egg; newEgg.name = "Errr ... Not Humpty Dumpty"; 

newEggを変更(オブジェクトを変更) newEggeggも自動的に変更されます。 それについてご存知ですか?

 console.log(egg); // { //   name: "Errr ... Not Humpty Dumpty" // } 

上記の例は、突然変異の危険性を示しています。 コード内の何かを変更すると、他の場所にあるものも変更される可能性があり、そのため、あなたもそれを知ることはできません。 その結果、検出および修正が困難なエラー。

これらの奇妙な点はすべて、JavaScriptのオブジェクトが参照によって渡されるという事実の結果です。

JavaScriptのオブジェクトとそれらへの参照


「オブジェクトは参照渡し」というステートメントの意味を理解するには、まずJavaScriptの各オブジェクトに一意の識別子があることを理解する必要があります。 オブジェクトを変数に割り当てる場合、オブジェクトの値を変数に書き込んでコピーする代わりに、変数をそのオブジェクトの識別子にバインドします(つまり、変数はオブジェクトを参照するようになります)。 そのため、2つの異なるオブジェクトを比較し、同じ値を含む(またはまったく含まない)場合でも、 falseになりfalse

 console.log({} === {}); // false 

上記の例で、 egg定数がnewEgg定数に割り当てられたnewEggegg定数によって参照される同じオブジェクトへのリンクがnewEggに書き込まれnewEggeggnewEggは同じオブジェクトを参照するため、 newEggが変更されると、 eggは自動的に変更されます。

 console.log(egg === newEgg); // true 

残念ながら、説明した状況と同様の状況では、1つの変数に書き込まれたものが別の変数にさらされたときに変更する必要は通常ありません。 それでは、オブジェクトの突然変異を防ぐ方法は? この質問に対する答えを見つける前に、まず、JSで不変である、つまり不変であるものを見つけておくとよいでしょう。

不変のプリミティブ


JavaScriptでは、プリミティブ( StringNumberBooleanNullUndefined 、およびSymbolデータ型について説明しています)は不変です。 つまり、プリミティブの構造を変更したり、プロパティやメソッドを追加したりすることはできません。 たとえば、プリミティブに新しいプロパティを追加しようとしても、まったく何も起こりません。

 const egg = "Humpty Dumpty"; egg.isBroken = false; console.log(egg); // Humpty Dumpty console.log(egg.isBroken); // undefined 

キーワードconstと免疫


constキーワードを使用して宣言された変数(定数)は不変であると多くの人が考えています。 しかし、これはそうではありません。

constキーワードを使用しても、定数に書き込まれるものは不変になりません。 定数に新しい値を割り当てることはできません。

 const myName = "Zell"; myName = "Triceratops"; // ERROR 

constキーワードを使用してオブジェクトを定義すると、その内部構造を完全に変更できます。 eggオブジェクトを使用した例では、 eggconstキーワードを使用して作成された定数ですが、このオブジェクトは突然変異を防ぎません。

 const egg = { name: "Humpty Dumpty" }; egg.isBroken = false; console.log(egg); // { //   name: "Humpty Dumpty", //   isBroken: false // } 

オブジェクト突然変異防止


オブジェクトの突然変異を防ぐために、オブジェクトを操作するときにObject.assignメソッドを使用できますObject.assignメソッドは、既存のオブジェクトを組み合わせてプロパティを結果オブジェクトに割り当てることにより、新しいオブジェクトを作成する操作を実装します。

▍Object.assignメソッド


Object.assignコンストラクトを使用Object.assignと、2つのオブジェクト(または複数のオブジェクト)を組み合わせて、1つの新しいオブジェクトを出力できます。 次のように使用できます。

 const newObject = Object.assign(object1, object2, object3, object4); 

newObjectは、 Object.assign渡されるすべてのオブジェクトのプロパティが含まれます。

 const papayaBlender = { canBlendPapaya: true }; const mangoBlender = { canBlendMango: true }; const fruitBlender = Object.assign(papayaBlender, mangoBlender); console.log(fruitBlender); // { //   canBlendPapaya: true, //   canBlendMango: true // } 

競合する2つのプロパティが見つかった場合、 Object.assignの引数リストの右側にあるオブジェクトのプロパティは、左側のリストにあるオブジェクトのプロパティを上書きします。

 const smallCupWithEar = { volume: 300, hasEar: true }; const largeCup = { volume: 500 }; //     volume  ,  300   500 const myIdealCup = Object.assign(smallCupWithEar, largeCup); console.log(myIdealCup); // { //   volume: 500, //   hasEar: true // } 

ただし、注意してください! Object.assignを使用して2つのオブジェクトを結合すると、引数リストの最初のオブジェクトが変更されます。 他の人はしません。

 console.log(smallCupWithEar); // { //   volume: 500, //   hasEar: true // } console.log(largeCup); // { //   volume: 500 // } 

ObjectObject.assignを使用する際の突然変異の問題の解決策


最初のObject.assignとして、既存のオブジェクトの突然変異を防ぐために新しいオブジェクトを渡すことができます。 ただし、最初のオブジェクト(空の)はまだ変更中ですが、突然変異は重要なものには影響しないため、心配する必要はありません。

 const smallCupWithEar = { volume: 300, hasEar: true }; const largeCup = { volume: 500 }; //        const myIdealCup = Object.assign({}, smallCupWithEar, largeCup); 

この操作を実行した後、必要に応じて新しいオブジェクトを変更できます。 これは以前のオブジェクトには影響しません。

 myIdealCup.picture = "Mickey Mouse"; console.log(myIdealCup); // { //   volume: 500, //   hasEar: true, //   picture: "Mickey Mouse" // } // smallCupWithEar   console.log(smallCupWithEar); // { volume: 300, hasEar: true } // largeCup   console.log(largeCup); // { volume: 500 } 

▍Object.assignおよびプロパティオブジェクトへのリンク


Object.assign 1つの問題は、浅いマージを実行することです。つまり、あるオブジェクトから別のオブジェクトにプロパティを直接コピーします。 同時に、処理されたオブジェクトのプロパティであるオブジェクトへのリンクもコピーします。

これを例として考えてください。

新しいサウンドシステムを購入したとします。 パワーを制御し、音量、低音レベル、その他のパラメーターを設定できます。 これが標準のシステム構成です。

 const defaultSettings = { power: true, soundSettings: {   volume: 50,   bass: 20,   //   } }; 

友人の中には大音量の音楽が好きな人もいるので、家全体が耳に届くことが保証されたプリセットを作成することにしました。

 const loudPreset = { soundSettings: {   volume: 100 } }; 

次に、友達をパーティーに招待します。 システムを稼働状態にし、同時に標準設定と音量を最大にする設定の両方を使用するには、 defaultSettingsloudPresetを組み合わせようとします。

 const partyPreset = Object.assign({}, defaultSettings, loudPreset); 

ただし、音楽をオンにすると、 partyPresetシステムの音がおかしいことがpartyPresetます。 ボリュームは良好ですが、低音はまったくありません。 partyPresetを探索するpartyPreset 、低音設定がないことに驚かれます!

 console.log(partyPreset); // { //   power: true, //   soundSettings: { //     volume: 100 //   } // } 

これは、JavaScriptが参照によってsoundSettingsプロパティsoundSettingsコピーするためです。 defaultSettingsloudPreset両方にsoundSettingsオブジェクトがあるため、 Object.assign引数の右側にあるオブジェクトが新しいオブジェクトにコピーされます。

partyPresetを変更partyPreset 、それに応じてpartyPresetが変化しますsoundSettingsからloudPresetへのリンクがコピーされた証拠として。

 partyPreset.soundSettings.bass = 50; console.log(loudPreset); // { //   soundSettings: { //     volume: 100, //     bass: 50 //   } // } 

Object.assignはオブジェクトのサーフェスマージを実行するため、同様の状況で、新しいオブジェクトがプロパティオブジェクトを含むオブジェクトの組み合わせである場合、他のものを使用する必要があります。 なに? たとえば、 assignmentライブラリ。

▍ライブラリの割り当て


Assignmentは、 Pony Foo (JSに関する貴重な情報源)のNicolas Bevacuaによって作成された小さなライブラリです。 オブジェクトのディープマージ(ディープマージ)を実行し、同時に突然変異を心配しないことが役立ちます。 assignment使用は、異なるメソッド名を使用することを除いて、 Object.assignでの作業と同じように見えます。

 //       assignment const partyPreset = assignment({}, defaultSettings, loudPreset); console.log(partyPreset); // { //   power: true, //   soundSettings: { //     volume: 100, //     bass: 20 //   } // } 

ライブラリは、他のオブジェクトに埋め込まれたすべてのオブジェクトの値を新しいオブジェクトにコピーし、既存のオブジェクトを変更から保護します。

partyPreset.soundSettingsプロパティを変更しようとしても、 loudPresetは変更されないことがわかります。

 partyPreset.soundSettings.bass = 50; // loudPreset   console.log(loudPreset); // { //   soundSettings { //     volume: 100 //   } // } 

assignmentライブラリは、オブジェクトを深くマージできる多くのツールの1つにすぎません。 lodash.assignmerge-optionsなどの他のライブラリもこれに役立ちます。 一番好きなものを安全に選択できます。

Object.assignではなく、ディープマージを常に使用する必要がありますか?


これでオブジェクトを突然変異から保護する方法がわかったので、 Object.assignを有意義に使用Object.assignます。 この標準的な方法を正しく使用する方法を知っていれば、何も問題はありません。

ただし、ネストされたプロパティを持つオブジェクトを使用する必要がある場合は、常にObject.assignではなくオブジェクトのディープマージを使用してください。

オブジェクトの耐性を確保する


上記で説明した方法は、オブジェクトを突然変異から保護するのに役立ちますが、それらの方法で作成されたオブジェクトの耐性を保証するものではありません。 入れ子になったオブジェクトプロパティを持つオブジェクトをObject.assignときに間違えてObject.assignを使用すると、後で深刻な問題が発生する可能性があります。

これから身を守るためには、オブジェクトがまったく変化しないことを保証する価値があります。 これにはImmutableJSのようなライブラリを使用できます。 このライブラリは、ヘルプを使用して処理されたオブジェクトを変更しようとするとエラーをスローします。

または、 Object.freezeメソッドとdeep-freezeライブラリを使用できます。 これらの2つのツールはエラーを生成しませんが、オブジェクトの変更も許可しません。

Object.freezeメソッドとディープフリーズライブラリ


Object.freezeメソッドは、オブジェクト自体のプロパティを変更から保護します。

 const egg = { name: "Humpty Dumpty", isBroken: false }; // ""  egg Object.freeze(egg); //          egg.isBroken = true; console.log(egg); // { name: "Humpty Dumpty", isBroken: false } 

ただし、 defaultSettings.soundSettings.baseなどの「凍結」オブジェクトのプロパティであるオブジェクトを変更しようとすると、このメソッドは役に立ちません。

 const defaultSettings = { power: true, soundSettings: {   volume: 50,   bass: 20 } }; Object.freeze(defaultSettings); defaultSettings.soundSettings.bass = 100; //    soundSettings  console.log(defaultSettings); // { //   power: true, //   soundSettings: { //     volume: 50, //     bass: 100 //   } // } 

プロパティオブジェクトの突然変異を防ぐために、オブジェクトである「フリーズ」オブジェクトのすべてのプロパティに対してObject.freezeを再帰的に呼び出すディープフリーズライブラリを使用できます。

 const defaultSettings = { power: true, soundSettings: {   volume: 50,   bass: 20 } }; //  " " (   deep-freeze) deepFreeze(defaultSettings); //      ,      defaultSettings.soundSettings.bass = 100; // soundSettings    console.log(defaultSettings); // { //   power: true, //   soundSettings: { //     volume: 50, //     bass: 20 //   } // } 

値と突然変異の書き換えについて


変数のエントリと、新しい値のオブジェクトのプロパティのエントリを突然変異と混同しないでください。
実際、新しい値が変数に書き込まれると、それが指すものが変わります。 次の例では、 aの値が11から100変更されます。

 let a = 11; a = 100; 

突然変異により、オブジェクト自体が変化します。 変数または定数に書き込まれたオブジェクトへの参照は同じままです。

 const egg = { name: "Humpty Dumpty" }; egg.isBroken = false; 

まとめ


突然変異は、コードを混乱させる可能性があり、これを行うために、まったく気付かないうちに予測できないため、危険です。 問題の原因が突然変異であると疑われる場合でも、問題のある場所を見つけることは依然として困難です。 したがって、不愉快な驚きからコードを保護する最良の方法は、オブジェクトの作成時から、突然変異からの保護を確実にすることです。

オブジェクトを突然変異から保護するには、 ImmutableJSMori.jsなどのライブラリを使用するか、標準のJSメソッドObject.assignおよびObject.freezeを使用できます。

Object.assign Object.freezeObject.freezeは、オブジェクトのネイティブプロパティのみを変更から保護できることに注意してください。 オブジェクトそのものである突然変異やプロパティから保護する必要がある場合は、 割り当てディープフリーズなどのライブラリが必要になります。

親愛なる読者! オブジェクトの突然変異によってJSアプリケーションで予期しないエラーが発生しましたか?

Source: https://habr.com/ru/post/J346998/


All Articles