オブジェクト一覧

JavaScriptでループを使わずに配列やオブジェクトをコピーする

配列やオブジェクトを扱っていると、元の状態の保存などの目的で、コピー(複製)を作成したいケースが出てきます。

例えば、写真加工アプリでは、リセット時に元の写真を素早く復元できるよう、読み込み時に画像のデータを格納したオブジェクトをコピーして記憶しておく、という設計が考えられます。

ループで1つずつ要素・プロパティをコピーしていく方法もありますが、行数が多くなるため、なるべく短く記述したいものです。

この記事では、ループを使わずに、JavaScriptで配列やオブジェクトをコピーするためのいくつかの方法と、その特徴や注意点についてまとめます。

配列やオブジェクトの代入はコピーにはならない

まず最初に、=を使った代入について触れておきます。

/* コピー対象が文字列の場合 */
 
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には、DateFunctionなど、復元できないオブジェクトの型が多くあります。

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などのオブジェクトのコピーにも対応していますが、完全ではありません。

まとめ

ここに挙げた以外にもコピーを作成する方法はありますが、特にディープコピーについては、上述のように、完全無欠な方法は無いという現状です。

コピーを行う際は、コピー対象の構造をよく確認して、適切なコピー方法を選択する、または独自のコピー処理を作成していく必要があります。