配列やオブジェクトを扱っていると、元の状態の保存などの目的で、コピー(複製)を作成したいケースが出てきます。
例えば、写真加工アプリでは、リセット時に元の写真を素早く復元できるよう、読み込み時に画像のデータを格納したオブジェクトをコピーして記憶しておく、という設計が考えられます。
ループで1つずつ要素・プロパティをコピーしていく方法もありますが、行数が多くなるため、なるべく短く記述したいものです。
この記事では、ループを使わずに、JavaScriptで配列やオブジェクトをコピーするためのいくつかの方法と、その特徴や注意点についてまとめます。
Contents
配列やオブジェクトの代入はコピーにはならない
まず最初に、=
を使った代入について触れておきます。
/* コピー対象が文字列の場合 */ let base = 'Taro'; let copy = base; base = 'Jiro'; console.log(base); // Jiro console.log(copy); // Taro
対象が数値や文字列の場合は、いわゆる値渡しとなるため、代入=コピーとなります。実体が別に作られているため、コピー元に変更を加えても、コピー先に影響はありません。
/* コピー対象がオブジェクトの場合 */ const base = { a: 0 }; const copy = base; base.a = 1; console.log(base); // { a: 1 } console.log(copy); // { a: 1 }
一方、対象が配列やオブジェクトの場合は、いわゆる参照渡しとなります。値渡しのように実体が別に作られているわけではないため、元のオブジェクトへの変更の影響を受けます。
つまり、代入によるコピーは、数値や文字列には使えますが、配列やオブジェクトには使えません。
シャローコピーとディープコピー
本題の配列やオブジェクトのコピーですが、大きく分けてシャローコピーとディープコピーの2種類があります。
対象が入れ子(ネスト)構造になっていた場合、ディープコピーではすべての階層について実体をコピーするのに対し、シャローコピーでは通常、最初の1階層のみ実体がコピーされます。
const base = { a: 0, b: { c: 'c' }}; // オブジェクトが入れ子となっている const shallow = Object.assign({}, base); const deep = JSON.parse(JSON.stringify(base)); // シャローコピー&ディープコピー(詳細は後述) base.a = 1; base.b.c = 'ccc'; // コピー元に変更を加える console.log(shallow); // { a: 0, b: { c: "ccc" }} console.log(deep); // { a: 0, b: { c: "c" }}
シャローコピーでは依然として、内側のオブジェクトについて、元のオブジェクトへの変更の影響を受けていることがわかります。
配列やオブジェクトの完全なコピーを必要とするケースでは、シャローコピーではなくディープコピーを行う必要があります。
配列やオブジェクトのシャローコピーを行う方法
配列を操作し、新たな配列を返すメソッド
配列には、配列に何らかの操作を行った後、新たな配列を返すメソッドがあります。これらを利用して、配列のコピーを作成することができます。
Array.prototype.slice
Array.prototype.concat
Array.prototype.map
Array.prototype.filter
Array.from
いずれも、得られるコピーは元の配列のシャローコピーとなります。
const copy01 = base.slice(0); const copy02 = base.concat(); const copy03 = base.map(function (elm) { return elm; }); const copy04 = base.filter(function () { return true; }); const copy05 = Array.from(base);
Object.assign
Object.assign
は、オブジェクトの結合に使うメソッドです。
const copy = Object.assign({}, base);
このように、空のオブジェクトに対しコピー元を結合することで、シャローコピーが生成されます。
スプレッド演算子(...
)
配列などを展開する演算子を使って、配列のコピーを作成することができます。
const copy = [...base];
展開された要素を新たな配列の要素として受け取ることで、配列のコピーが生成されます。また、配列リテラルではなく、Array.ofを使うことも可能です。
const copy = Array.of(...base);
new Array(...base)
とArray
コンストラクタを使うこともできますが、new Array(3)
のように、引数が0
や自然数1つのときに動作が変わるので、要注意です。
それから、スプレッド演算子は元々、配列のみで使える構文でしたが、ES2018からはオブジェクトでも使えるようになりました。
const base = { a: 0 }; const copy = { ...base };
最新の構文であるため、EdgeやIEで利用できないことに注意が必要です。
配列やオブジェクトのディープコピーを作成する方法
JSON.stringify
+ JSON.parse
JSON.stringify
は配列やオブジェクトを文字列化するメソッド、JSON.parse
はJSON形式の文字列を配列やオブジェクトに復元するメソッドです。
この2つのメソッドを足し合わせて、配列やオブジェクトをコピーすることができます。
const base = { a: 0, b: { c: 0 } }; const copy = JSON.parse(JSON.stringify(base)); base.b.c = 2; console.log(copy); // { a: 0, b: { c: 0 } }
ディープコピーであるため、コピー元の内側のオブジェクトに変更を加えても、コピー先では影響を受けません。
注意点として、配列の要素がundefined
の場合はnull
に変換され、オブジェクトのプロパティがundefined
の場合はそのプロパティ自体が無視されます。
const base1 = [0, undefined, null]; const base2 = { a: 0, b: undefined, c: null }; const copy1 = JSON.parse(JSON.stringify(base1)); const copy2 = JSON.parse(JSON.stringify(base2)); console.log(copy1); // [0, null, null] console.log(copy2); // {a: 0, c: null}
また、JSON.parse
には、Date
やFunction
など、復元できないオブジェクトの型が多くあります。
const base = { name: 'Taro', birthday: new Date(1990, 0, 1) }; const copy = JSON.parse(JSON.stringify(base)); console.log(typeof base.birthday); // object console.log(typeof copy.birthday); // string
このようなケースでは完全なコピーとはなりませんので、原則としてundefined
ではなくnull
を使う、値は数値や文字列、boolean値(true/false)のみとするなど注意が必要です。
jQueryのextendメソッド
extend
は複数のオブジェクトを結合して拡張するメソッドです。第1引数をtrue
、第2引数を空のオブジェクトにすることで、ディープコピーとして動作します。
const copy = $.extend(true, {}, base);
前項の方法とは異なり、Date
オブジェクトなど、いくつかの型のオブジェクトのコピーにも対応しています。
const base = { name: 'Taro', birthday: new Date(2002, 4, 5) }; const copy = $.extend(true, {}, base); console.log(copy.birthday instanceof Date); // true
なお、第1引数を省略するとシャローコピーとなります。false
とはできません。
LodashのcloneDeepメソッド
Lodashにもコピーを作成するためのメソッドがあります。
const copy = _.cloneDeep(base);
jQuery同様、Date
などのオブジェクトのコピーにも対応していますが、完全ではありません。
まとめ
ここに挙げた以外にもコピーを作成する方法はありますが、特にディープコピーについては、上述のように、完全無欠な方法は無いという現状です。
コピーを行う際は、コピー対象の構造をよく確認して、適切なコピー方法を選択する、または独自のコピー処理を作成していく必要があります。