〽️ 2つの違いを知っておくべき。
〽️ 「解らない部分」をピンポイントで解説。
JavaScriptコードの「?」をまとめたページはこちら⬆︎。
こんにちは、「ふ」です。
今回はJavaScriptの「shallow(浅い)コピー」「deep(深い)コピー」についてお話しします。
「浅い/深いコピー?」なんぞや?
以前の記事で、スプレッド構文を使ったオブジェクトや配列の複製について紹介しました。
これは「浅いコピー」に相当します。
const obj1 = { a:"AAA" }; const obj2 = {...obj1};
obj1のコピーとしてobj2を作成しました。
と、ここでobj2の内容を変更してみます。
obj2.a = "BBB"; console.log(obj1); 「「3▶︎ {a:"AAA"}」」 console.log(obj2); 「「3▶︎ {a:"BBB"}」」
obj2のプロパティaの内容を変更しました。
その後obj1,obj2を出力しています。obj2の変更は、obj1に影響をあたえていません。
〜ここまではよし。
次は少し複雑なオブジェクトになっています。
const obj3 = { a:"AAA", num:[1,2,3] };
obj3の2つ目のプロパティ「num」には配列が格納されています。
これを複製し、配列部分を変更してみましょう。
const obj4 = {...obj3}; obj4.num[0] = 2;
複製したobj4の、プロパティ「num」の1番目の要素を変更しました。
その上で、obj3とobj4を出力。
console.log(obj3); 「「3▶︎ {a:"AAA",num:[2,2,3]}」」 console.log(obj4); 「「3▶︎ {a:"AAA",num:[2,2,3]}」」
すると。
obj4の配列が変更されているのはわかるのですが、obj3の配列まで内容がかわってしまっています。
これは、numの値である配列の中身が「参照渡し」になっているからです。
let a = {A:1,B:2}; let b = a;
通常、配列やオブジェクトを格納した変数を別の変数に渡すと、「参照」のみが渡され、2つの変数はメモリ上の同じ記録領域を共有します。
オブジェクトのプロパティ単位でも、同様のことが起きます。親のオブジェクトをそのまま複製すると、単一のプロパティ値は値渡し、配列などの複合データで構成されているプロパティ値は参照渡しになってしまうのです。
参照先としてメモリ上の記録が共有されてしまっているため、「コピー先/コピー元の値が完全には独立していない」といえます。
こういったコピー形式はshallow(浅い)コピーと呼ばれています。
オブジェクトの内容を階層的にイメージしてみると、プロパティ直下の値に対しては独立した複製がされているのですが、それより深い階層である配列要素に対しては独立複製ができていません。
「浅い階層しか独立コピーできていない」と言う意味で、shallowという呼び名がついているのです。
const arr1 = ["a","b",[1,2,3]]; const arr2 = [...arr1];
スプレッド構文で配列を複製した場合にも、shallowコピーとなります。 ⬆︎の例では、arr1の3つ目の要素[1,2,3]は参照渡しでarr2に格納されます。
JavaScriptでは通常、複製処理を行うと基本的にはshallowコピーが採用されます。それに対し、コピー元の深い階層の内容も独立した形で行うコピーが、deep(深い)コピーとよばれるものです。
ではdeepコピーを行うには、どのようにすればいいのか。
それが.... JavaScriptの正式な文法としては、すんなりdeepコピーを行う構文は用意されていません💧
なので、以下2つの方法を使うことになります。
・JS0Nデータを使った、文字列としてのやりとり。
・ブラウザのstructuredClone( )メソッド。
〜これらについて、紹介していきます。
〽️ 本質は「オブジェクトの複製」にあり。
〽️ メモリの仕組みを知っておきましょう。
参照渡しやスプレッド構文については、⬆︎の記事で詳しく解説しています。
元オブジェクトの内容は、「プロパティ:値」という意味付けがなされたものです。意味を持たせた状態で他の変数などに渡すと当然、JavaScriptの解析ルールにより「shallowな複製」として扱われてしまいます。
そこで、オブジェクトの記述をを意味を持たない「ただの文字列データ」に変換させてから、obj2に渡すことにしましょう。
とはいっても、渡した先で再びオブジェクトに戻すことができなければ、意味がありません。そのため「JSONデータ」というものに変換します。
JSONデータはJavaScriptの記述形式をもとにした「文字列データ」で、プログラム言語間でのデータの受け渡しなどに使用されるものです。
JavaScriptの文法で記述されたものを他の言語に渡しても、先方でそのまま使うことはできません。
そのため一度、JSONデータという単なる「文字列」に変換して渡すようにしています。文字列であれば、大抵の言語は受け取ることができます。
そして受け取った側で使用可能なデータ型にparseして使用する、といった流れです。
JavaScript内の変数間においても、オブジェクトなどをJSON形式にして受け渡しを行えば、オブジェクトや配列などの集合データとして解析されてしまうことはありません。
オブジェクトをJSONに書き換えるには、JSON.stringify( )メソッドを使います。
const obj3 = { a:"AAA", num:[1,2,3] } 「「3//JSONデータに置き換え」」 const j_obj3 = 「「5JSON.stringify」」(obj3); console.log(j_obj3); 「「3▶︎ {"a":"AAA","num":[1,2,3]}」」
先ほどのobj3をJSONに変換し、console.logしてみました。
変換前のオブジェクトリテラルとほぼ同じものが出力されました。ただプロパティ名もクオテーションで囲まれています。これは、JSONデータの記述形式です。
console.log(typeof(j_obj3)); 「「3▶︎ string」」
このj_obj3のデータ型をしらべると「string」と出ます。
ちゃんと文字列に変換されていますね。
受け取ったJSONデータをJavaScriptで使用できるデータ型にもどすには、JSON.parse( )メソッドを使用します。
「「3//JavaScriptのデータ形式に戻す」」 let p_obj3 = JSON.parse(j_obj3); console.log(p_obj3); 「「3▶︎ {a:"AAA",num:[1,2,3]}」」 console.log(typeof(p_obj3)); 「「3▶︎ object」」
変数p_obj3を作り、先ほどのJSONデータをparse(解析)したものを格納しました。
出力してみると、オブジェクトに戻っているようです。念の為データ型も確認。
「object」と出ました。文字列のJSONデータがちゃんとオブジェクトとして解析されているのが確認できます。
JSONデータへの変換と解析ができるようになりました。
オブジェクト同士で内容をそのままやり取りした場合には「参照渡し」の機能が発動してしまいます。
しかし意味を持たないただの文字列であるJSONデータに変換しておけば、「参照渡し」の機能を発動させることなくオブジェクトの「深いコピー」ができそうです。
const obj3 = { a:"AAA", num:[1,2,3] };
それではJSONデータを使って、先ほどshallowコピーしかできなかった「obj3」のdeepコピーに挑戦しましょう。
const obj5 = JSON.parse(JSON.stringify(obj3));
1行でJSON.stringifyとJSON.parseを行い、obj5を作っています。
これでちゃんと「深いコピー」ができているか、検証。obj5のnum配列の内容を変更してみます。
obj5.num[0] = 2; console.log(obj5); 「「3▶︎ {a:'AAA',num:[「「42」」,2,3]}」」
obh5のnum配列を変更させました。その上でコピー元であるobj3をみてみます。
console.log(obj3); 「「3▶︎ {a:'AAA',num:[「「41」」,2,3]}」」
obj3のnum配列は変更されていません。
これは深い階層においても、obj3とobj5の内容は独立していることを意味します。
JSONデータを利用することで、全てが値渡しとなる「deepコピー」を行うことができました。
deepコピーを行うもう1つの方法は、structureClone( )メソッドを使用するやり方です。
structuredCloneはブラウザのwindowオブジェクトに実装されているメソッドで、JavaScript本来の文法として存在するものではありません。ただし主要ブラウザのほとんどがサポートしているため、通常のメソッドのように扱っても問題ありません。
const arr1 = [ [1,2], 3 ];
配列の中に配列が含まれている、「深い階層」を持ったもので試してみましょう。
const arr2 = 「「1structuredClone」」(arr1);
これでOKです。
JSONデータを使うよりも簡単ですね。では深い階層を変更して、deepコピーができているか検証してみましょう。
arr2[0][0] = 2; console.log(arr2); 「「3▶︎ (2) [Array(3), 4] 0:(3) [「「42」」, 2, 3] 1:4」」 console.log(arr1); 「「3▶︎ (2) [Array(3), 4] 0:(3) [「「41」」, 2, 3] 1:4」」
arr2の深い階層を変更しても、arr1には影響していません。
deepコピーができていますね。
ここまで話しておいて..なのですが、紹介してきた2つの方法〜JSONデータやstructuredClone( )を使ったdeepコピーは、「完全なコピー」とも言えないのです。
オブジェクトのメソッドなどは複製してくれません。
const obja = { A:function() { console.log("hello") } }
"hello"と出力するメソッドを含む、obja。
これをJSONデータに変更した場合、
console.log(JSON.stringify(obja)); 「「3▶︎ {}」」
メソッドの部分が無視されて、何も入っていないオブジェクトとされてしまうのです。同じくstructuredClone( )を使って複製した場合にも、errorとなってしまいます。
ちなみにスプレッド構文を使った場合には、メソッドもちゃんと反映されます。
const objb = {...obja}; objb.A(); 「「3▶︎ hello」」
第1階層 | 第2階層以降 | メソッド | |
shallowコピー | ○ | × | ○ |
deepコピー | ○ | ○ | × |
つまりはshallowコピーとdeepコピーにおいて、できることとできないことがそれぞれ存在するのです。
もし全ての条件を満たした「完全コピー」を実現させたい場合には、コンストラクタなどのテンプレートを用意する必要があります。そうなってくると「手軽なコピー」とは言えない作業が発生してしまうので、プログラムの設計段階から複製の可能性について、検討しておくべきでしょう。
最後までお読みくださり、ありがとうございました。
プログラミング学習を進めるにつれ、より高度で複雑なものを作りたくなってくるでしょう。そのときに思わぬ不具合を出してしまわないよう、今回紹介した「shallowコピー」「deepコピー」の違いについて理解を深めておいてください。
ではまた〜 ♪
ベクターグラフィック、web、ガジェットなど。役立つ情報や観ていてたのしいページを書いていきたいと思います。