LoginSignup
10
7

More than 5 years have passed since last update.

JavaScript:Array,Object,Map,Set,Dateをまとめてディープコピーする

Last updated at Posted at 2018-11-05

以前の記事の続きです。
あれからいろいろ考えて、今はこれがいいかな?と思っています。

引数の中味をあまり気にしないでディープコピーできる関数cloneです。
(さんざんディープコピーって言ってるのに関数名がcloneって...ディープコピー = クローンと読み替えるかしてお読みください。)

const mapForMap = f => a => 
  new Map( [...a].map( ([key, value]) =>[key, f(value)] ) );  
const mapForSet = f => a => new Set( [...a].map( f ) );
const entriesMapIntoObj = f => xs => 
  xs.reduce(
    (acc, [key, value]) => ({ ...acc, [key]:f(value) })
    , {} 
  );
const mapForObj = f => a =>
  entriesMapIntoObj( f )( Object.entries( a ) );
const getType = a => Object.prototype.toString.call(a);

const clone = a => {
  const type = getType( a );
  return (type === "[object Array]")?  a.map( clone )
        :(type === "[object Map]")?    mapForMap( clone )( a )
        :(type === "[object Set]")?    mapForSet( clone )( a )
        :(type === "[object Object]")? mapForObj( clone )( a )
        :(type === "[object Date]")?   new Date( a )
        : a;
}
//使用例:
const object = { 
   a: 1, 
   b: 'a', 
   c: '', 
   d: null, 
   e: undefined, 
   f: true, 
   g: [1, 2, 3], 
   h: function () { console.log('h'); }, 
   i: { 
     a: 1, 
     b: 'a', 
     c: '', 
     d: null, 
     e: undefined, 
     f: true, 
     g: [1, 2, 3], 
     h: function () { console.log('h'); }, 
     i: { a: 1 }, 
     j: new Map([ [ 0, 0 ], [ 1, 1 ] ]) ,
     k: new myClass("Q"),
     l: new Set( [ 1, 'string', {1:1,2:2} ] ),
     m: new Date()
   },
   j: new Map([ [ 0, 0 ], [ 1, 1 ] ]) ,
   k: new myClass("Q"),
   l: new Set( [ 1, 'string', {1:1,2:2} ] ),
   m: new Date()
}

const cloned = clone(object);

for(const e in object) object[e] = 0; // Objectのすべての要素の値を0にする
object
//=>
{ a: 0, b: 0, c: 0, d: 0, e: 0, f: 0, g: 0, h: 0, i: 0, j: 0, k: 0, l: 0, m: 0 }

cloned
//=>ディープコピーされているので、objectが変更されてもclonedは変わらない
{ a: 1,
  b: 'a',
  c: '',
  d: null,
  e: undefined,
  f: true,
  g: [ 1, 2, 3 ],
  h: [Function: h],
  i: 
   { a: 1,
     b: 'a',
     c: '',
     d: null,
     e: undefined,
     f: true,
     g: [ 1, 2, 3 ],
     h: [Function: h],
     i: { a: 1 },
     j: Map { 0 => 0, 1 => 1 },
     k: { name: 'Q' },
     l: Set { 1, 'string', [Object] },
     m: 2018-11-04T16:57:38.609Z },
  j: Map { 0 => 0, 1 => 1 },
  k: { name: 'Q' },
  l: Set { 1, 'string', { 1: 1, 2: 2 } },
  m: 2018-11-04T16:57:38.609Z }

階層が深いところの表示が[Object]に省略されていますが、

console.log(cloned.i.l);
//=> Set { 1, 'string', { 1: 1, 2: 2 } }

とちゃんとコピーされています。

どうなっている?

一応、予定している入力は、Array、Map、Set、Object、Dateとプリミティブ(数、文字列、真偽値、undefined、null)と関数です。これだけあれば十分でしょ?

仕組み自体は簡単です。

  • 引数 a の型を取得して
  • それぞれの型専用のディープコピー関数を適用する

これだけ。コードにすると:

const clone = a => {
  const type = getType( a );
  return (type === "[object Array]")?  cloneArray( a )
        :(type === "[object Map]")?    cloneMap( a )
        :(type === "[object Set]")?    cloneSet( a )
        :(type === "[object Object]")? cloneObj( a )
        :(type === "[object Date]")?   cloneDate( a )
        : cloneOthers( a );
}

それっぽく名前をつけただけなので、それぞれの型専用のディープコピー関数を作っていきます。
まずは簡単なところから。

cloneOthers

ここにはArrayでもMapでもSetでもObjectでもDateでもないものが来ます。
プリミティブ(数、文字列、真偽値、undefined、null)と、関数もここに入ります。
これらはそのまま返されます。

const cloneOthers = a => a;

今のところ、予定していない何かが来た場合もここでそのまま返されます。
実用上問題ないのでそうしていますが、問題があれば要改造(その何か専用のclone関数を作る)です。

cloneDate

a を元に新しいDateオブジェクトを作ります。

const cloneDate = a => new Date( a );

Array,Map,Set,Objectの場合

内部のそれぞれの要素をcloneで再帰的にディープコピーします。
配列にはmapが使えますが、他には直接は使えません。
それぞれ、map的なことができる関数を定義します。

const cloneArray = a => a.map( clone );
const cloneMap = a => mapForMap( clone )( a );
const cloneSet = a => mapForSet( clone )( a );
const cloneObj = a => mapForObj( clone )( a )

mapForMap,mapForSet

Map、Setはスプレッド構文で一旦配列にしてmapすることができます。
mapした配列を元に新しいMap,Setを作ります。

//[...a]で[key, value]の配列にし各要素のvalueにfを適用した配列を元に、新しいMapを作る
const mapForMap = f => a => 
  new Map( [...a].map( ([key, value]) =>[key, f(value)] ) );  

const mapForSet = f => a => new Set( [...a].map( f ) );

mapForObj

Objectは、entries()で[key, value]の配列に変換して、reduceでvalueにfを適用しつつ、空のObjectに要素を加えていき、新しいObjectを作ります。

// [key, value]の配列 xs の各要素の value に f を適用してObjectにする
const entriesMapIntoObj = f => xs => 
  xs.reduce(
    (acc, [key, value]) => ({ ...acc, [key]:f(value) })
    , {} 
  );
//aを[key, value]の配列に変換して、entriesMapIntoObj( f )を適用する
const mapForObj = f => a =>
  entriesMapIntoObj( f )( Object.entries( a ) );

※ fromEntries()が現時点であまり実装されてないようなのでこんなことしてます。

getType

Array,Map,Set Objectを区別するにはObject.prototype.toString.call()を使うそうです。

const getType = a => Object.prototype.toString.call(a);

返ってきた文字列で分岐します。

これで全部の部品が揃いました。 組合せると冒頭のコードになります。

何か勘違い、間違い、もっといい方法等あればコメントよろしくです。

10
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
7