Javascriptはどのようにオブジェクトを深くコピーしますか?

73894 ワード

誘引子
jsでは、どのようにオブジェクトをコピーしますか?みんなの第一反応は直接に賦課文を使って評価しますか?
let a = {a: 1};
let b = a;
印刷結果を見てください
console.log(a)  // {a: 1}
console.log(b)  // {a: 1}
プリントしたのは全部{a:1}です.いいですが、これは本当にコピーしたものですか?もう一度試してみます.
a.b = 1;
console.log(a)  // {a: 1, b: 1}
console.log(b)  // {a: 1, b: 1}
このようにaオブジェクトだけを修正しましたが、bオブジェクトもフォローして変更されました.これは何の原因ですか?
jsのタイプ
もともと、jsには2つのタイプの概念があり、それぞれ基本タイプと引用タイプであり、基本タイプと引用タイプの最も主要な違いはコンピュータの保存位置にあるということである.
基本タイプ:NumberStringBoolennullundefined・、Symbol・、Bigint引用タイプ:ObjectArray・、RegExp・・Date・・・Function
基本的なタイプはスタックに格納され、以下の特性を有する.
  • 基本タイプの比較はそれらの値の比較
  • 基本タイプ値をコピーすると、新たなメモリ空間が開発され、値を新たなメモリ空間にコピーする
  • 引用タイプはヒープに保存されています.ちょっと特性があります.
  • 引用タイプの比較は住所の比較(つまりポインタが指す内容が一致しているかどうか)
  • 参照タイプ値は、ヒープメモリに保存されているオブジェクトであり、変数が格納されているのは、そのメモリのアドレスを指すだけであり、参照タイプの値をコピーする場合は、そのメモリを指すアドレスのみがコピーされている
  • これにより、基本型のコピーは、直接に割り当てられた語句を使用することができ、このように直接コピーすると、新しいポインタがアドレスに向けられ、新しいオブジェクトがコピーされません.新しいオブジェクトをコピーする必要がある場合、どうすればいいですか?
    js対象のコピー
    jsのコピーは、浅いコピーと深いコピーに分けられます.
    浅いコピー:
           ,                    。         ,           ,         ,          ,                 ,          。
    
    浅いコピーの使用シーン:
    1.Object.assign()
    Object.assign()メソッドは、列挙可能な属性のすべての値を1つまたは複数のソースオブジェクトから対象オブジェクトにコピーするために使用されます.ターゲットオブジェクトに戻ります.Object.assign()は深くコピーしていないので、あえて言うならば、彼はコピー対象の第一層の基本タイプです.引用タイプはコピーしているのですか?それともポインタですか?以下の例を見ます.
    let obj1 = {
        a: 1,
        b: {c: 2}
    }
    let obj2 = Object.assign({}, obj1);
    console.log(obj2);
    // {
    // 	a: 1,
    // 	b: {c: 2}
    // } 
    
    obj1.a = 3;
    obj1.b.c = 4;
    console.log(obj1);
    // {
    // 	a: 3,
    // 	b: { c: 4}
    // } 
    
    console.log(b);
    // {
    // 	a: 1,
    // 	b: {c: 4}
    // } 
    // } 
    
             obj1   ,   obj2          。        obj1      b  ,   obj2            。
    
    2.演算子を展開する…
    let obj1 = {
        a: 1,
        b: {c: 2}
    }
    
    let obj2 = {...obj1}
    console.log(obj2);  // {a: 1, b: {c: 2}}
    
    obj1.a = 3;
    obj1.b.c = 4;
    
    console.log(obj1); // {a: 3, b: {c: 4}}
    console.log(obj2); // {a: 1, b: {c: 4}}
    
    上のコードで見た演算子を展開します.実際の効果はObject.assignと同じです.
    3.アラy.prototype.slice()、アラy.prototype.com()
    let arr1 = [1,2,[3,4]];
    let arr2 = arr1.slice(1);
    console.log(arr2); // [2,[3,4]]
    
    arr1[1] = 5;
    arr1[2][0] = 6;
    console.log(arr1); // [1,5,[6,4]]
    console.log(arr2); // [2,[6,4]]
    
    let arr1 = [1,[2,3]];
    let arr2 = [4,5,6];
    let arr3 = arr1.concat(arr2);
    console.log(arr3); // [1,[2,3],4,5,6]
    
    arr1[0] = 7;
    arr1[1][0] = 8;
    console.log(arr1); // [7,[8,3]]
    console.log(arr3); // [1,[8,3],4,5,6]
    
    Arayのsliceconcat方法も深くコピーされていないことが分かりますので、複雑な配列を扱う際には注意が必要です.
    コピー:
    深度コピーはすべての属性をコピーし、属性が指す動的に割り当てられたメモリをコピーします.オブジェクトが参照されているオブジェクトと一緒にコピーされると、深くコピーされます.深度コピーは、浅いコピーよりも遅く、かつ、より多くの費用がかかります.コピー前後の二つのオブジェクトは互いに影響しない.
    以上のように、参照対象がポインタである以上、基本タイプは値を格納し、参照タイプを基本タイプに変更し、この基本タイプを参照タイプに変換して再割り当てすることで、参照タイプを深くコピーする効果がある.
    let a = {a: 1};
    let b = JSON.stringify(a);
    let c = JSON.parse(b);
    
    console.log(a); // {a: 1}
    console.log(b); // {a: 1}
    
    a.b = 2;
    
    console.log(a); // {a: 1, b: 2}
    console.log(b)  // {a: 1}
    
    このようにして、私達は新しい相手を得ました.すべては完璧に見えますが、相手が複雑な時、また新しい問題を発見しました.
    let a = {
      a: "1",
      b: undefined,
      c: Symbol("dd"),
      fn: function() {
        return true;
      },
    };
    console.log(JSON.stringify(a)); // {a: 1}
    
    emmy、aオブジェクトは3つの値があるのに、なぜJSON.stingifyの後に1つだけ現れましたか?
    JSON.strigifyは以下の特性を持っています.
    undefined、smbol、関数の3つの場合は、直接無視されます.
     let obj = {
        name: 'muyiy',
        a: undefined,
        b: Symbol('muyiy'),
        c: function() {}
    }
    console.log(obj);  // { name: "muyiy", a: undefined, b: Symbol(muyiy), c: ƒ () }
    
    let b = JSON.parse(JSON.stringify(obj));
    console.log(b);  // {name: "muyiy"}
    
    循環参照の場合、エラーが発生します.
    let obj = {
        a: 1,
        b: {
            c: 2,
            d: 3
        }
    }
    obj.a = obj.b;
    obj.b.c = obj.a;
    
    let b = JSON.parse(JSON.stringify(obj));  // Uncaught TypeError: Converting circular structure to JSON
    
    new Dateの場合、変換結果が不正です.
    new Date();
    // Mon Dec 24 2018 10:59:14 GMT+0800 (China Standard Time)
    
    JSON.stringify(new Date());
    // ""2018-12-24T02:59:25.776Z""
    
    JSON.parse(JSON.stringify(new Date()));
    // "2018-12-24T02:59:41.523Z"
    
    正則を解くことができない
    let obj = {
        name: "muyiy",
        a: /'123'/
    }
    console.log(obj);
    // {name: "muyiy", a: /'123'/}
    
    let b = JSON.parse(JSON.stringify(obj));
    console.log(b);
    // {name: "muyiy", a: {}}
    
    じゃ、私達はどうやってこのような状況を避けるべきですか?
    実は、1つの対象の深度コピーを実現して、彼を2つの部分に分けることができて、つまり浅いコピー+再帰、現在の属性が対象かどうかを判断することができて、対象なら再帰的に操作します.
    function deepClone1(obj) {
        var target = {};
        for(var key in obj) {
            if( typeof obj[key] === 'object') {
                target[key] = deepClone1(obj[key]);
            } else {
                target[key] = obj[key];
            }
        }
        return target;
    }
    
    let obj1 = {a: 1, b: {c: 2}};
    let obj2 = deepClone1(obj1);
    console.log(obj2); // {a: 1, b: {c: 2}}
    
    obj1.a = 3;
    obj1.b.c = 4;
    
    console.log(obj1); // {a: 3, b: {c: 4}}
    console.log(obj2); // {a: 1, b: {c: 2}}
    
    以上は、深いコピーの簡単な実現ですが、複雑な、多種類のオブジェクトに直面して、上記の方法はまだ多くの欠陥があります.
    1.nullのことを考えていない
    jsのデザインでは、objectの上位3桁のマークは000で、nullも32桁の表示では全部0ですので、typeof nullもプリントアウトします.object
    function deepClone2(obj) {
        if (obj === null) return null; //     ,  obj   null
        var target = {};
        for(var key in obj) {
            if( typeof obj[key] === 'object') {
                target[key] = deepClone2(obj[key]);
            } else {
                target[key] = obj[key];
            }
        }
        return target;
    }
    
    2.配列の互換性を考慮していません.
    jsでは、typeof配列も一つのobjectであり、配列に対して処理を行う必要がある.
    function deepClone3(obj) {
        if (obj === null) return null;
        var target = Array.isArray(obj) ? []: {}; //     ,       
        for(var key in obj) {
            if( typeof obj[key] === 'object') {
                target[key] = deepClone3(obj[key]);
            } else {
                target[key] = obj[key];
            }
        }
        return target;
    }
    
    
    3.対象中の循環参照を考慮していない場合
    実は循環参照を解決するという考え方は、現在の値が既に存在しているかどうかを決める前に、循環参照を避けるために、ここではS 6のWeakMapを使ってhashテーブルを生成することができます.
    function deepClone4(obj, hash = new WeakMap()) {
        if (obj === null) return null;
        if (hash.has(obj)) return hash.get(obj); //     ,    
        var target = Array.isArray(obj) ? []: {};
        hash.set(obj, target); //     ,     
        for(var key in obj) {
            if( typeof obj[key] === 'object') {
                target[key] = deepClone4(obj[key], hash); //   hash 
            } else {
                target[key] = obj[key];
            }
        }
        return target;
    }
    
    var a = {b: 1};
    a.c = a;
    console.log(a); // {b:1, c: {b: 1, c:{......}}}
    var b = deepClone4(a);
    console.log(b); // {b:1, c: {b: 1, c:{......}}}
    
    es 5であれば、同じ配列でも可能です.
    4.Symbolは考えていません.
             Symbol,       Object.getOwnPropertySymbols()  Reflect.ownKeys(),  ,    Object.getOwnPropertySymbols()     Symbol   
    
    function deepClone5(obj, hash = new WeakMap()) {
        if (obj === null) return null;
        if (hash.has(obj)) return hash.get(obj);
        var target = Array.isArray(obj) ? []: {};
        hash.set(obj, target);
        // =============     
        let symKeys = Object.getOwnPropertySymbols(obj); //   
        if (symKeys.length) { //     	
            symKeys.forEach(symKey => {
                if (typeof obj[symKey] === 'object') {
                    target[symKey] = deepClone5(obj[symKey], hash); 
                } else {
                    target[symKey] = obj[symKey];
                }    
            });
        }
        // =============
        for(var key in obj) {
            if( typeof obj[key] === 'object') {
                target[key] = deepClone5(obj[key], hash);
            } else {
                target[key] = obj[key];
            }
        }
        return target;
    }
    
    5.es 6のMapとSetのコピー
    typeof Map/Setオブジェクトもobjectなので、ここでObject.prototype.toStering.callを使用する必要があります.以下、deep Clone関数を改造する必要があります.
    function deepClone6(obj, hash = new WeakMap()) {
        //      null
        if (obj === null) return null;
        //   hash ,         
        if (hash.has(obj)) return hash.get(obj);
        //   Symbol
        let symKeys = Object.getOwnPropertySymbols(obj);
        if (symKeys.length) {
            symKeys.forEach(symKey =>{
                if (typeof obj[symKey] === 'object') {
                    target[symKey] = deepClone6(obj[symKey], hash);
                } else {
                    target[symKey] = obj[symKey];
                }
            });
        }
        //        ,      ,     ,     ,     
        if (typeof obj === 'object') {
            let target = null;
            let result;
            hash.set(obj, target);
            let objType = Object.prototype.toString.call(obj);
            switch (objType) {
            case '[object Object]':
                target = {};
                break;
            case '[object Array]':
                target = [];
                break;
            case '[object Map]':
                //   Map  
                result = new Map();
                obj.forEach((value, key) =>{
                    result.set(key, deepClone6(value, hash))
                }) 
                return result
                break;
            case '[object Set]':
                //   Set  
                obj.forEach((value) =>{
                    result.add(deepClone6(value, hash))
                }) 
                return result
                break;
            default:
                break;
            }
        } else {
            //        
            return obj;
        }
        for (var key in obj) {
            if (typeof obj[key] === 'object') {
                target[key] = deepClone6(obj[key], hash);
            } else {
                target[key] = obj[key];
            }
        }
        return target;
    }
    
    6.Dateオブジェクト、正則、および関数
    Dateオブジェクトの複製は、直接に新しいnew Date()オブジェクトに戻り、setTime、setYearなどによる参照の変更を避けることができます.正則、および関数は参照対象ですが、山にも保存されています.ただし、一般的には属性は付けられませんので、ここでは直接に値を付与すればいいです.
    function deepClone7(obj, hash = new WeakMap()) {
        //      null
        if (obj === null) return null;
        //   hash ,         
        if (hash.has(obj)) return hash.get(obj);
        //   Symbol
        let symKeys = Object.getOwnPropertySymbols(obj);
        if (symKeys.length) {
            symKeys.forEach(symKey =>{
                if (typeof obj[symKey] === 'object') {
                    target[symKey] = deepClone7(obj[symKey], hash);
                } else {
                    target[symKey] = obj[symKey];
                }
            });
        }
        //        ,      ,     ,     ,     
        if (typeof obj === 'object' || typeof obj === 'function') {
            let target = null;
            let result;
            hash.set(obj, target);
            let objType = Object.prototype.toString.call(obj);
            switch (objType) {
            case '[object Object]':
                target = {};
                break;
            case '[object Array]':
                target = [];
                break;
            case '[object Map]':
                //   Map  
                result = new Map();
                obj.forEach((value, key) =>{
                    result.set(key, deepClone7(value, hash))
                }) 
                return result
                break;
            case '[object Set]':
                //   Set  
                obj.forEach((value) =>{
                    result.add(deepClone7(value, hash))
                }) 
                return result
                break;
            case '[object Date]':
                //   Date  
                return new Date(obj)
                break;
            default:
                //       、  
                return obj;
                break;
            }
        } else {
            //        
            return obj;
        }
        for (var key in obj) {
            if (typeof obj[key] === 'object') {
                target[key] = deepClone7(obj[key], hash);
            } else {
                target[key] = obj[key];
            }
        }
        return target;
    }
    
    7.再帰的な爆発を避ける
    上の深いコピーは再帰的に使うので、一般的に再帰的にメモリを大量に消費し、爆発的なスタックが存在することが分かります.この弊害に対して、私たちは通常二つの解決の考えがあります.一つは後戻りで、もう一つは再帰を深さ遍歴または広さに転化することです.最後に、簡単に構想を提供します.
    function cloneDeep8(x) {
        const root = {};
    
        //  
        const loopList = [
            {
                parent: root,
                key: undefined,
                data: x,
            }
        ];
    
        while(loopList.length) {
            //     
            const node = loopList.pop();
            const parent = node.parent;
            const key = node.key;
            const data = node.data;
    
            //        ,key undefined       ,        
            let res = parent;
            if (typeof key !== 'undefined') {
                res = parent[key] = {};
            }
    
            for(let k in data) {
                if (data.hasOwnProperty(k)) {
                    if (typeof data[k] === 'object') {
                        //      
                        loopList.push({
                            parent: res,
                            key: k,
                            data: data[k],
                        });
                    } else {
                        res[k] = data[k];
                    }
                }
            }
        }
        return root;
    }