以前の記事の続きです。
あれからいろいろ考えて、今はこれがいいかな?と思っています。
引数の中味をあまり気にしないでディープコピーできる関数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);
返ってきた文字列で分岐します。
これで全部の部品が揃いました。 組合せると冒頭のコードになります。
何か勘違い、間違い、もっといい方法等あればコメントよろしくです。