web-dev-qa-db-ja.com

循環参照を持つJavascriptディープクローンオブジェクト

Dmitriy Pichuginによる 既存の回答 から以下の関数をコピーしました。この関数は、循環参照なしでオブジェクトをディープクローンできます-機能します。

function deepClone( obj ) {
    if( !obj || true == obj ) //this also handles boolean as true and false
        return obj;
    var objType = typeof( obj );
    if( "number" == objType || "string" == objType ) // add your immutables here
        return obj;
    var result = Array.isArray( obj ) ? [] : !obj.constructor ? {} : new obj.constructor();
    if( obj instanceof Map )
        for( var key of obj.keys() )
            result.set( key, deepClone( obj.get( key ) ) );
    for( var key in obj )
    if( obj.hasOwnProperty( key ) )
            result[key] = deepClone( obj[ key ] );
    return result;
}

ただし、プログラムは無限にループし、循環参照が原因であることに気付きました。

循環参照の例:

function A() {}
function B() {}

var a = new A();
var b = new B();

a.b = b;
b.a = a;
14
Lolums

Mapを使用して、コピー元のオブジェクトをコピー先のオブジェクトにマップすることをお勧めします。実際、Bergiの提案に従って WeakMap を使用してしまいました。ソースオブジェクトがマップ内にあるときはいつでも、それ以上再帰するのではなく、対応するコピーが返されます。

同時に、元のdeepCloneコードのコードの一部をさらに最適化できます。

  • プリミティブ値をテストする最初の部分には小さな問題があります。それはnew Number(1)new Number(2)とは異なる方法で処理します。これは、最初のifの_==_が原因です。 _===_に変更する必要があります。しかし、実際には、コードの最初の数行はこのテストと同等のように見えます:Object(obj) !== obj

  • いくつかのforループをより機能的な式に書き直しました

これにはES6のサポートが必要です。

_function deepClone(obj, hash = new WeakMap()) {
    // Do not try to clone primitives or functions
    if (Object(obj) !== obj || obj instanceof Function) return obj;
    if (hash.has(obj)) return hash.get(obj); // Cyclic reference
    try { // Try to run constructor (without arguments, as we don't know them)
        var result = new obj.constructor();
    } catch(e) { // Constructor failed, create object without running the constructor
        result = Object.create(Object.getPrototypeOf(obj));
    }
    // Optional: support for some standard constructors (extend as desired)
    if (obj instanceof Map)
        Array.from(obj, ([key, val]) => result.set(deepClone(key, hash), 
                                                   deepClone(val, hash)) );
    else if (obj instanceof Set)
        Array.from(obj, (key) => result.add(deepClone(key, hash)) );
    // Register in hash    
    hash.set(obj, result);
    // Clone and assign enumerable own properties recursively
    return Object.assign(result, ...Object.keys(obj).map (
        key => ({ [key]: deepClone(obj[key], hash) }) ));
}
// Sample data
function A() {}
function B() {}
var a = new A();
var b = new B();
a.b = b;
b.a = a;
// Test it
var c = deepClone(a);
console.log('a' in c.b.a.b); // true_
11
trincot

オブジェクトの複製には多くの落とし穴があるため(循環参照、プロトチェーン、セット/マップなど)
よくテストされた人気のあるソリューションの1つを使用することをお勧めします。

lodash's _.cloneDeep または 'clone' npm module のように。

7
artin

参照と結果を別々の配列に格納でき、同じ参照を持つプロパティが見つかった場合は、キャッシュされた結果を返すだけで済みます。

function deepClone(o) {
    var references = [];
    var cachedResults = [];

    function clone(obj) {
        if (typeof obj !== 'object')
            return obj;
        var index = references.indexOf(obj);
        if (index !== -1)
            return cachedResults[index];
        references.Push(obj);
        var result = Array.isArray(obj) ? [] :
            obj.constructor ? new obj.constructor() : {};
        cachedResults.Push(result);
        for (var key in obj)
            if (obj.hasOwnProperty(key))
                result[key] = clone(obj[key]);
        return result;
    }
    return clone(o);
}

マップと他のタイプの比較のいくつかを削除して、読みやすくしました。

最新のブラウザをターゲットにできる場合は、@ trincotの確かなES6回答を確認してください。

1
A1rPun