[JavaScript] 色々な型をディープコピーする（getter, setter, symbol 対応）

# はじめに

JavaScript のオブジェクトをディープコピーする方法は，過去にも色々と紹介されています [\[1\]][1]．
しかし，getter, setter, symbol をコピーするものが見つからなかったので関数を自作しました．
enumerable が false のプロパティもコピーできます．
ES6 での利用を想定しています．

実装するにあたり，[ttatsfさんの記事][2] [\[2\]][2] と[MDNのページ][3] [\[3\]][3] を参考にさせていただきました．
ありがとうございます．


## 対応している型

下のテーブル以外の型をコピーすると，シャローコピー（参照がコピーされる）されたり，エラーが投げられるかもしれません．

| type | 対応 |
|:--|:-:|
| primitive | ○  |
| Object | ○  |
| Array | ○  |
| Map  | ○  |
| Set  | ○  |
| Function  | ○  |
| GeneratorFunction  | ○  |
| ArrayBuffer  | △  |
| TypedArray  | △  |
| DataView  | △  |

△ ⋅⋅⋅ 実験的


# 1. 実行環境

Chrome デベロッパーツールのコンソールで動作検証しました．

<table>
<tr><td>OS<td>Windows 10 OS Version 1909 (Build 18363.1440)
<tr><td>JavaScript<td>V8 8.9.255.24
<tr><td>Google Chrome<td>89.0.4389.114 (Official Build) （64 bit）
</table>


# 2. コード
```JavaScript:deepCopy.js
const deepCopy = a => {
    let self = a;
    let reTypedArray = /(?:U?[Ii]nt|Float)(?:8|16|32)(?:Clamped)?Array/;
    let type = typeofFinely(a);
    return (type === 'Array')?   a.map(deepCopy)
          :(type === 'Map')?     new Map( [...a].map(deepCopy) )
          :(type === 'Set')?     new Set( [...a].map(deepCopy) )
          :(type === 'Date')?    new Date(a)
          :(type === 'Function')? a
          :(type === 'Object')?  mapForObject(deepCopy)(a)
          :(type === 'ArrayBuffer')?    a.slice(0)
          :(type.match(reTypedArray))?  new a.constructor(a)
          :(type === 'DataView')?       new DataView(a.buffer.slice(0), a.byteOffset, a.byteLength)
          : a;
}

// typeof はオブジェクトの細かい区別ができないので
const typeofFinely = x => Object.prototype.toString.call(x).slice(8, -1);

// 'Object' 専用
const mapForObject = f => x => {
    let descriptors =
        // Object.getOwnPropertyNames() ではシンボルを取得できない
        [...Object.getOwnPropertyNames(x), ...Object.getOwnPropertySymbols(x)]
            .reduce( (acc, key) => {
                let descriptor = Object.getOwnPropertyDescriptor(x, key);
                // ディスクリプタが value プロパティをもつなら，value に f (deepCopy) を適用
                if (descriptor.hasOwnProperty('value'))
                       descriptor.value = f(descriptor.value);
                else {
                    if (descriptor.hasOwnProperty('get'))
                           descriptor.get = f(descriptor.get);
                    if (descriptor.hasOwnProperty('set'))
                           descriptor.set = f(descriptor.set);
                }
                Object.defineProperty(acc, key, descriptor);
                return acc;
            }, {});

    return descriptors;
};
```

## 2-1. 解説

詳しい解説を読みたい方は [ttatsfさんの記事][2] [\[2\]][2] と [MDNのページ][3] [\[3\]][3] を見てください．
以下，簡単な解説をします．

1. `typofFinely()`でオブジェクトの詳細な「型」を取得

2. 取得した「型」に応じて`deepCopy()`内部の処理を分ける
  - Array : `deepCopy()`を`map()`した新しいインスタンスを返す  
  <br>
  - Map, Set : スプレッド演算子で展開したあとに，`deepCopy()`を`map()`した新しいインスタンスを返す  
  <br>
  - Object : `mapForObjcet()`を適用して，プロパティをコピーした新しいインスタンスを返す
     - コピー元のオブジェクトのディスクリプタを`descripters`に追加していく　
     - ディスクリプタのキーに`value`があれば，`deepCopy()`を呼び出す
     - 最後に`Object.defineProperties()`で新しい Object のインスタンスを返す  
     <br>
  - Date : `new Date()`の引数にして新しいインスタンスを返す  
  <br>
  - 上記以外 (primitiveなど) : そのまま返す 
  <br>

<details><summary>ディスクリプタ（記述子）について [\[4\]][4]</summary><ul><li>プロパティが「値」をもつ場合
	<ul><li>データ記述子になり value をキーとしてもつ</ul>
<li>getter や setter の場合
	<ul><li>アクセサー記述子になり get か set, またはその両方をキーとしてもつ
</details>

## 2-2. 動作検証

```JavaScript:test
var sym = Symbol();
// コピー元
var source = {
    // getter, setter, symbol
    get getter() { return this.obj.a },
    set setter(a) { this.obj.a = a },
    [sym]: 'symbol',
    // objects
    obj: { a: 1, b: 2, c: 3 },
    arr: [1, 2, 3],
    map: new Map( [['a', 1], ['b', 2], ['c', 3]] ),
    set: new Set( ['alpha', 'beta', 'ganma'] ),
    date: new Date( Date.UTC(1970, 0, 0, 0, 0, 0) ),
    // primitives
    bool: true,
    Null: null,
    undef: undefined,
    num: 1,
    bigint: BigInt(10**10),
    str: 'string',
    symbol: Symbol(),
    // functions
    func: function(){},
    arrFunc: ()=>{},
    generator: function*(){},
    // wrapper
    Bool: new Boolean(true),
    Num: new Number(1),
    Str: new String('string'),
    // arraybuffer, typedarray, dataview
    arrBuff: new ArrayBuffer(8),
    typedArr: new Int16Array(8),
    dataView: new DataView(new ArrayBuffer(8)),
    // etc
    nan: NaN,
    inf: Infinity,
    zero: 0,
    False: false,
    empStr: "",
    empArr: [],
    empObj: {},
};

var copied = deepCopy(source); // ディープコピー！！
copied.setter = 10;

console.log(
`source.getter: ${source.getter}
copied.getter: ${copied.getter}`);
console.log(`copied[sym] === source[sym]: ${copied[sym] === source[sym]}`);
    // 期待する出力
    //   source.getter: 1
    //   copied.getter: 10
    //   copied[sym] === source[sym]: true

// source のプロパティを変更しても copied に影響を与えないことを確認する
[...Object.getOwnPropertyNames(source), ...Object.getOwnPropertySymbols(source)]
    .forEach( key => source[key] = undefined );
Object.defineProperties(source, { getter: { get: undefined },
                                  setter: { set: undefined }, });

//// copied の出力
[...Object.getOwnPropertyNames(copied), ...Object.getOwnPropertySymbols(copied)]
    .forEach( key => console.log('%s: %o', key, copied[key]));
```

### 2-2-1. 実行結果
```JavaScript:testOut
source.getter: 1
copied.getter: 10
copied[sym] === source[sym]: true

//// copied の出力（出力順序は一部変更）
// getter, setter, symbol
getter: 10
setter: undefined
Symbol(): "symbol"
// objects
obj: {a: 10, b: 2, c: 3}
arr: (3) [1, 2, 3]
map: Map(3) {"a" => 1, "b" => 2, "c" => 3}
set: Set(3) {"alpha", "beta", "ganma"}
date: Wed Dec 31 1969 09:00:00 GMT+0900 (日本標準時)
// primitives
bool: true
Null: null
undef: undefined
num: 1
bigint: 10000000000n
str: "string"
symbol: Symbol()
// functions
func: ƒ (){}
arrFunc: ()=>{}
generator: ƒ* (){}
// wrapper
Bool: Boolean {true}
Num: Number {1}
Str: String {"string"}
// arraybuffer, typedarray, dataview
arrBuff: ArrayBuffer(8) {}
typedArr: Int16Array(8) [0, 0, 0, 0, 0, 0, 0, 0]
dataView: DataView(8) {}
// etc
nan: NaN
inf: Infinity
zero: 0
False: false
empStr: ""
empArr: []
empObj: {}
```

# （3. プロトタイプもコピーするオプション機能付き - 実装中）
```JavaScript:deepCopyOpt.js
// オプション
var o = { copyGetter: false, copySetter: false, copySymbol: false };
// ラッパー
const deepCopyOpt = (a, options = {}) => {
    let __options = {
        copyGetter: true,
        copySetter: true,
        copySymbol: true,
        copyPrototypeAll: false,
        copyPrototypeDepth: -1,
        copyPrototypeUpTo: -1,
        copyNonEnumerableProperty: true,
    };
    Object.assign(__options, options);

    let copied = __deepCopyOpt(a, __options);
    return (options.copyPrototypeAll)
               ? copied
               : (options.copyPrototypeUpTo < 0)
                     ? copied
                     : deepCopyPrototype( copied, a, Object.defineProperty(__options, '__isCalledByDCP', { value: true }) ); 
}
    
const __deepCopyOpt = (a, options, depth=0) => {
console.log('depth: '+depth);
    let type = typeofFinely(a);
    let copied = (type === 'Array')?   a.map( bindFromNthArg(__deepCopyOpt)(options, depth+1) )
                :(type === 'Map')?     new Map( [...a].map( bindFromNthArg(__deepCopyOpt)(options, depth+1) ) )
                :(type === 'Set')?     new Set( [...a].map( bindFromNthArg(__deepCopyOpt)(options, depth+1) ) )
                :(type === 'Date')?    new Date(a)
                :(type === 'Object')?  mapForObjectOpt(__deepCopyOpt)(a, options, depth+1)
                : a;

// デバッグ用出力
//console.log('depth2: '+depth);
console.log('a: %o', a);
//console.log('a.__proto__: %o', Object.getPrototypeOf(a));
console.log('copied: %o', copied);
console.log('optProtoUpto: %o', options.copyPrototypeUpTo);
//console.log('optProtoDepth: %o', options.copyPrototypeDepth);
//options.copyPrototypeDepth < 0 || options.copyPrototypeDepth--;

    // a==null || copied==null || aの型がオブジェクトでない || copyProtoが-1以下
    return (options.copyPrototypeAll || options.__isCalledByDCP)
               ? deepCopyPrototype(copied, a, options)
               : copied;
/*
        ( !a || !copied || typeof(a) !== 'object' || options.copyPrototypeUpTo < 0 )
               ? (console.log(1), copied)
               // copyProto!=null && aのプロトタイプ!=copyProto && aのプロトタイプ!=null && aのプロトタイプの型==object
               : ( options.copyPrototypeUpTo && Reflect.getPrototypeOf(a) !== options.copyPrototypeUpTo && Reflect.getPrototypeOf(a) && typeof(Reflect.getPrototypeOf(a)) === 'object' )
                     ? ( typeof(options.copyPrototypeUpTo) === 'number' && options.copyPrototypeUpTo--, console.log(2),
                         Object.setPrototypeOf(copied, __deepCopyOpt(Reflect.getPrototypeOf(a), __options)) )
                     : ( console.log(3), Object.setPrototypeOf(copied, Reflect.getPrototypeOf(a)) );
*/
}
const deepCopyPrototype = (copied, source, options) => {
    // a==null || copied==null || aの型がオブジェクトでない || copyProtoが-1以下
    return ( !source || !copied || typeof(source) !== 'object' || options.copyPrototypeUpTo < 0 )
               ? copied
               // copyProto!=null && aのプロトタイプ!=copyProto && aのプロトタイプ!=null && aのプロトタイプの型==object
               : ( options.copyPrototypeUpTo && Reflect.getPrototypeOf(source) !== options.copyPrototypeUpTo && Reflect.getPrototypeOf(source) && typeof(Reflect.getPrototypeOf(source)) === 'object' )
                     ? ( typeof(options.copyPrototypeUpTo) === 'number' && options.copyPrototypeUpTo--,
                         Object.setPrototypeOf(copied, __deepCopyOpt(Reflect.getPrototypeOf(source), options)) )
                     : Object.setPrototypeOf( copied, Reflect.getPrototypeOf(source) );

}
const bindFromNthArg = (f, n = 1) => (...boundArgs) => (...args) => f(...args.slice(0, n), ...boundArgs, ...args.slice(n));  
const typeofFinely = x => Object.prototype.toString.call(x).slice(8, -1);
const mapForObjectOpt = f => (x, options, depth) => {
    let descriptors =
        Object.getOwnPropertyNames(x).reduce( (acc, key) => {
            let descripter = Reflect.getOwnPropertyDescriptor(x, key);
            // ( 記述子が value をキーにもつ
            //     && ( 列挙可能 || オプション copyNotEnumerable == true ) )
            if ( descripter.hasOwnProperty('value')
                && (descripter.enumerable || options.copyNonEnumerableProperty) )
                       descripter.value = f(descripter.value, options, depth);
            ( descripter.hasOwnProperty('get') && !options.copyGetter )
                && delete descripter.get;
            ( descripter.hasOwnProperty('set') && !options.copySetter )
                && delete descripter.set;
            acc[key] = descripter;
            return acc;
        }, {});

    if (options.copySymbol)
        Object.getOwnPropertySymbols(x).forEach(sym => {
            let descripter = Object.getOwnPropertyDescriptor(x, sym);
            if (descripter.enumerable || options.copyNonEnumerableProperty)
                   descriptors[sym] = descripter;
        });

    return Object.defineProperties({}, descriptors);
};
const range = (start, stop, step = 1) => Array.from({ length: (stop - start) / step + 1}, (_, i) => start + (i * step));
const applyNtimes = (f, n) => (...args) =>  range(1, n).reduce( acc => acc = f(acc), ...args);


a={a:{aa:1}, b:{bb:2}};
b={b:2};
c={c:3};
d={d:4};
Object.setPrototypeOf(a, b);
Object.setPrototypeOf(b, c);
A=deepCopyOpt(a, {copyPrototypeUpTo: Infinity, copyPrototypeDepth: 3, copyPrototyepAll: true});
```


# おわりに
誤りや改善点があればコメントお願いします．

# 参考
<a id="1"></a>
[1]:https://qiita.com/knhr__/items/d7de463bf9013d5d3dc0
\[1\] [[JavaScript]色々なディープコピー - Qiita][1]
[2]:https://qiita.com/ttatsf/items/7f2891d0b78ba36c2dca
\[2\] [JavaScript: Array,Map,Object,Setをディープコピーする関数 - Qiita][2]
[3]:https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Copying_accessors
\[3\] [Object.assign() - JavaScript | MDN][3]
[4]:https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#Description
\[4\] [Object.defineProperty() - JavaScript | MDN][4]