オブジェクトとプリミティブの基本的な違いの 1 つは、オブジェクトは「参照によって」保存およびコピーされるのに対し、プリミティブ値 (文字列、数値、ブール値など) は常に「値全体として」コピーされることです。
値をコピーするときに何が起こるかを少し詳しく見てみると、それは簡単に理解できます。
文字列などのプリミティブから始めましょう。
ここでは、 message
のコピーをphrase
に置き換えます。
let message = "こんにちは!"; let フレーズ = メッセージ;
結果として、2 つの独立変数があり、それぞれに文字列"Hello!"
が格納されます。 。
当然の結果ですね。
物体はそういうものではありません。
オブジェクトに割り当てられた変数には、オブジェクト自体ではなく、その「メモリ内のアドレス」、つまりオブジェクトへの「参照」が格納されます。
このような変数の例を見てみましょう。
ユーザー = { にします 名前:「ジョン」 };
実際にメモリに保存される方法は次のとおりです。
オブジェクトはメモリ内のどこか (図の右側) に保存されますが、 user
変数 (左側) にはそれへの「参照」があります。
user
などのオブジェクト変数は、オブジェクトのアドレスが記載された紙のようなものと考えることができます。
オブジェクトに対してアクションを実行するとき、たとえばプロパティuser.name
を取得するとき、JavaScript エンジンはそのアドレスにあるものを調べ、実際のオブジェクトに対して操作を実行します。
なぜそれが重要なのかを説明します。
オブジェクト変数がコピーされると、参照もコピーされますが、オブジェクト自体は複製されません。
例えば:
ユーザー = { 名前: "ジョン" }; 管理者 = ユーザーとします。 // 参照をコピーします
これで 2 つの変数ができ、それぞれに同じオブジェクトへの参照が格納されました。
ご覧のとおり、オブジェクトはまだ 1 つありますが、それを参照する変数が 2 つあります。
どちらの変数も使用してオブジェクトにアクセスし、その内容を変更できます。
ユーザー = { 名前: 'ジョン' }; 管理者 = ユーザーとします。 admin.name = 'ピート'; // "admin" 参照によって変更されます アラート(ユーザー名); // 'Pete'、変更は "user" 参照から確認されます
これは、2 つのキーを持つキャビネットがあり、そのうちの 1 つ ( admin
) を使用してそのキャビネットに入り、変更を加えるようなものです。その後、後で別のキー ( user
) を使用すると、同じキャビネットを開いたままになり、変更されたコンテンツにアクセスできます。
2 つのオブジェクトが等しいのは、それらが同じオブジェクトである場合のみです。
たとえば、ここではa
とb
同じオブジェクトを参照しているため、それらは等しいです。
a = {} にします。 b = a とします。 // 参照をコピーします アラート( a == b ); // true、両方の変数が同じオブジェクトを参照する アラート( a === b ); // 真実
そして、ここでは 2 つの独立したオブジェクトは、似ているように見えても等価ではありません (両方とも空です)。
a = {} にします。 b = {} とします。 // 2 つの独立したオブジェクト アラート( a == b ); // 間違い
obj1 > obj2
のような比較、またはプリミティブobj == 5
との比較の場合、オブジェクトはプリミティブに変換されます。オブジェクト変換がどのように機能するかについてはすぐに学習しますが、実を言うと、そのような比較が必要になることは非常にまれです。通常、それらはプログラミング上の間違いの結果として発生します。
Const オブジェクトは変更可能
オブジェクトを参照として保存することの重要な副作用は、 const
として宣言されたオブジェクトが変更される可能性があることです。
例えば:
const ユーザー = { 名前:「ジョン」 }; user.name = "ピート"; // (*) アラート(ユーザー名); // ピート
行(*)
によってエラーが発生するように見えるかもしれませんが、そうではありません。 user
の値は定数であり、常に同じオブジェクトを参照する必要がありますが、そのオブジェクトのプロパティは自由に変更できます。
言い換えれば、 const user
、全体としてuser=...
を設定しようとした場合にのみエラーを返します。
とはいえ、本当にオブジェクトのプロパティを定数にする必要がある場合は、まったく別のメソッドを使用することも可能です。これについては、「プロパティ フラグと記述子」の章で説明します。
したがって、オブジェクト変数をコピーすると、同じオブジェクトへの参照がもう 1 つ作成されます。
しかし、オブジェクトを複製する必要がある場合はどうすればよいでしょうか?
新しいオブジェクトを作成し、そのプロパティを反復処理してプリミティブ レベルでコピーすることで、既存のオブジェクトの構造を複製できます。
このような:
ユーザー = { にします 名前:「ジョン」、 年齢: 30歳 }; クローン = {} を作成します。 // 新しい空のオブジェクト // すべてのユーザー プロパティをそこにコピーしましょう for (ユーザーにキーを入力させます) { クローン[キー] = ユーザー[キー]; } // これで、クローンは同じ内容を持つ完全に独立したオブジェクトになります clone.name = "ピート"; // 中のデータを変更しました アラート( ユーザー名 ); // 元のオブジェクトにはまだ John が残っています
Object.assign メソッドを使用することもできます。
構文は次のとおりです。
Object.assign(dest, ...sources)
第一引数dest
対象オブジェクトです。
さらなる引数はソース オブジェクトのリストです。
すべてのソース オブジェクトのプロパティをターゲットdest
にコピーし、それを結果として返します。
たとえば、 user
オブジェクトがあるので、それにいくつかの権限を追加しましょう。
ユーザー = { 名前: "ジョン" }; let 許可 1 = { canView: true }; let 権限 2 = { canEdit : true }; // すべてのプロパティを Permission1 と Permission2 から user にコピーします Object.assign(ユーザー、権限1、権限2); // 現在 user = { name: "John", canView: true, canEdit: true } アラート(ユーザー名); // ジョン アラート(user.canView); // 真実 アラート(user.canEdit); // 真実
コピーされたプロパティ名がすでに存在する場合は上書きされます。
ユーザー = { 名前: "ジョン" }; Object.assign(user, { 名前: "ピート" }); アラート(ユーザー名); // 現在ユーザー = { 名前: "ピート" }
Object.assign
を使用して、単純なオブジェクトの複製を実行することもできます。
ユーザー = { にします 名前:「ジョン」、 年齢: 30歳 }; let clone = Object.assign({}, user); アラート(clone.name); // ジョン アラート(clone.age); // 30
ここでは、 user
のすべてのプロパティを空のオブジェクトにコピーして返します。
オブジェクトのクローンを作成する他の方法もあります。たとえば、チュートリアルで後ほど説明するスプレッド構文clone = {...user}
を使用します。
これまで、 user
のプロパティはすべてプリミティブであると想定していました。ただし、プロパティは他のオブジェクトへの参照にすることができます。
このような:
ユーザー = { にします 名前:「ジョン」、 サイズ: { 身長: 182、 幅: 50 } }; アラート( user.sizes.height ); // 182
user.sizes
はオブジェクトであり、参照によってコピーされるため、 clone.sizes = user.sizes
コピーするだけでは十分ではありません。そのため、 clone
とuser
同じサイズを共有します。
ユーザー = { にします 名前:「ジョン」、 サイズ: { 身長: 182、 幅: 50 } }; let clone = Object.assign({}, user); alert( user.sizes === clone.sizes ); // true、同じオブジェクト // ユーザーとクローンの共有サイズ user.sizes.width = 60; // プロパティを 1 か所から変更します アラート(clone.sizes.width); // 60、もう一方から結果を取得します
これを修正してuser
とclone
完全に別個のオブジェクトにするには、 user[key]
の各値を検査するクローン作成ループを使用し、オブジェクトの場合はその構造も複製する必要があります。これは、「ディープ クローン作成」または「構造化クローン作成」と呼ばれます。ディープ クローン作成を実装する StructuredClone メソッドがあります。
structuredClone(object)
呼び出しは、すべてのネストされたプロパティを含むobject
のクローンを作成します。
この例での使用方法は次のとおりです。
ユーザー = { にします 名前:「ジョン」、 サイズ: { 身長: 182、 幅: 50 } }; let clone = StructuredClone(user); alert( user.sizes === clone.sizes ); // false、別のオブジェクト // ユーザーとクローンは完全に無関係になりました user.sizes.width = 60; // プロパティを 1 か所から変更します アラート(clone.sizes.width); // 50、関係ありません
structuredClone
メソッドは、オブジェクト、配列、プリミティブ値など、ほとんどのデータ型のクローンを作成できます。
また、オブジェクト プロパティがオブジェクト自体を (直接またはチェーンや参照を介して) 参照する場合、循環参照もサポートします。
例えば:
ユーザー = {} にします。 // 循環参照を作成しましょう: // user.me はユーザー自体を参照します user.me = ユーザー; let clone = StructuredClone(user); アラート(clone.me === クローン); // 真実
ご覧のとおり、 clone.me
user
ではなくclone
を参照します。したがって、循環参照も正しく複製されました。
ただし、 structuredClone
失敗する場合もあります。
たとえば、オブジェクトに関数プロパティがある場合:
// エラー 構造化クローン({ f: 関数() {} });
関数のプロパティはサポートされていません。
このような複雑なケースを処理するには、クローン作成メソッドを組み合わせて使用するか、カスタム コードを記述するか、車輪の再発明を避けるために既存の実装 (たとえば、JavaScript ライブラリ lodash の _.cloneDeep(obj) など) を使用する必要がある場合があります。
オブジェクトは参照によって割り当ておよびコピーされます。つまり、変数には「オブジェクトの値」ではなく、その値の「参照」(メモリ内のアドレス)が格納されます。したがって、そのような変数をコピーするか、関数の引数として渡すと、オブジェクト自体ではなく、その参照がコピーされます。
コピーされた参照によるすべての操作 (プロパティの追加/削除など) は、同じ単一オブジェクトに対して実行されます。
「実際のコピー」(クローン)を作成するには、いわゆる「浅いコピー」(ネストされたオブジェクトは参照によってコピーされます)または「深いクローン作成」関数のObject.assign
structuredClone
使用するか、またはカスタム クローン作成実装を使用します。 _.cloneDeep(obj) として。