js實現深拷貝

淺拷貝

當把數組或對象簡單賦值給其他變量的時候,實際上進行的是淺拷貝,淺拷貝是拷貝引用,只是將拷貝後的引用指向同一個對象實例,彼此間的操作還會互相影響。

1. 使用 for in 遍歷

簡單的拷貝對象的屬性,如果屬性是基礎類型則沒有問題,但是對象是對象類型(數組,函數)則只是拷貝的是引用,改變原始的屬性,拷貝出來的屬性也會跟着改變

 function shallowCopy(source){
    var target=source instanceof Array ? [] : {};
    for(var i in source){
   // 使用 hasOwnProperty 方法判斷屬性是否存在
        if(source.hasOwnProperty(i)){
            target[i]=source[i];
        }
    }
    return target;
}

// 測試
var obj={
  a:1,
  b:[1,2,3],
  c:function(){
      console.log('i am c')}
      }
var tar=shallowCopy(obj)
tar.c()         // "i am c"
obj.a=5
obj.a           // 5
tar.a           // 1
obj.b[0]=10
obj.b           // [10, 2, 3]
tar.b           // [10, 2, 3]

var arr=[1,2,[4,5,6]]
var newArr=shallowCopy(arr)
newArr          // [1, 2, [4,5,6]]
arr[0]=10
arr             // [10, 2, [4,5,6]]
newArr          // [1, 2, [4,5,6]]
arr[2][0]=10
arr             // [1, 2, [10,5,6]]
newArr          // [1, 2, [10,5,6]]

2.0bject . assign

Object.assign()拷貝的是屬性值。假如源對象的屬性值是一個對象的引用,那麼它也只指向那個引用。也就是說,如果對象的屬性值爲簡單類型(如string, number),通過Object.assign({},srcObj);得到的新對象爲深拷貝;如果屬性值爲對象或其它引用類型,那對於這個對象而言其實是淺拷貝的。

var obj={a:1,b:[1,2,3],c:function(){console.log('i am c')}}
var tar={};
Object.assign(tar,obj);
//測試
obj.b[2]=1
console.log(obj.b)//[ 1, 2, 1 ]
console.log(tar.b)//[ 1, 2, 1 ]

3. 數組方法實現數組淺拷貝

  • Array.prototype.slice
var arr=[1,2,[3,4]];
var newArr=arr.slice(0);
  • Array.prototype.concat
var arr=[1,2,[3,4]];
var newArr=arr.concat();
  • Array.from
var arr=[1,2,[3,4]];
var newArr= Array.from(arr);
console.log(newArr)//[ 1, 2, [ 3, 4 ] ]

深拷貝

在堆中重新分配內存,並且把源對象所有屬性都進行新建拷貝,拷貝後的對象與原來的對象完全隔離,互不影響。

1. 對象的序列化與反序列化

let obj = { name: 'aaa', age: 30, action: function () { } }
console.log(JSON.stringify(obj))  //轉json字符串
let str = '{"name":"aaa","age":30}'
let obj2 = JSON.parse(JSON.stringify(obj)) //把json字符串轉爲json對象
obj2.name = "bbb";
console.log(obj, obj2)

存在的坑

  • 如果對象中有函數有方法就不能實現拷貝了
  • 會拋棄對象的constructor,所有的構造函數會指向Object
  • 對象有循環引用,會報錯
// 構造函數
function person(pname) {
  this.name = pname;
}

const Messi = new person('Messi');

// 函數
function say() {
  console.log('hi');
};

const oldObj = {
  a: say,
  b: new Array(1),
  c: new RegExp('ab+c', 'i'),
  d: Messi
};

const newObj = JSON.parse(JSON.stringify(oldObj));

// 無法複製函數
console.log(newObj.a, oldObj.a); // undefined [Function: say]
// 稀疏數組複製錯誤
console.log(newObj.b[0], oldObj.b[0]); // null undefined
// 無法複製正則對象
console.log(newObj.c, oldObj.c); // {} /ab+c/i
// 構造函數指向錯誤
console.log(newObj.d.constructor, oldObj.d.constructor); // [Function: Object] [Function: person]

//對象循環引用會報錯
const oldObj = {};

oldObj.a = oldObj;

const newObj = JSON.parse(JSON.stringify(oldObj));
console.log(newObj.a, oldObj.a); // TypeError: Converting circular structure to JSON


使用遞歸完成深拷貝

需要遞歸判斷對象的屬性是什麼數據類型,逐一進行拷貝

思路:
typeof運算符的操作對象是一個函數時,得到的是 “function” 所以在循環裏第一個if判斷那爲false 所以走else分支,在tar[i] = obj[i]這裏,函數是進行引用賦值的,如果再造一個相同的函數不是不可以,只是不符合思想罷了,函數佔用堆內存,如果可以共用當然是最好的選擇。

function deep(dest, ori) { //dest目標對象  ori源對象
    for (var i in ori) {
        if (typeof ori[i] === 'object') {
            //遞歸   判斷是數組還是對像
            dest[i] = (ori[i].constructor === Array) ? [] : {}; //初始化屬性
            deep(dest[i], ori[i])
        } else {
            dest[i] = ori[i];  //非引用屬性
        }
    }
    return dest;
}
//測試
function simple(obj) {
    var o = {};
    Object.assign(o, obj);   //目標對象   源對象
    return o;
}
// var a = simple(Animal);
// var b = simple(Animal);
var a = deep({}, Animal)
var b = deep({}, Animal)
a.name = "tom";
a.skin.push("white");
// a.say();
// b.say();
console.log(a, b)

但是下面的情況,會循環調用,會陷入一個循環的遞歸過程,從而導致爆棧,只需要判斷一個對象的字段是否引用了這個對象或這個對象的任意父級即可

var obj1 = {
    x: 1, 
    y: 2
};
obj1.z = obj1;

var obj2 = deepCopy(obj1);

解決

function deepCopy(obj, parent = null) {
    // 創建一個新對象
    let result = {};
    let keys = Object.keys(obj),
        key = null,
        temp= null,
        _parent = parent;
    // 該字段有父級則需要追溯該字段的父級
    while (_parent) {
        // 如果該字段引用了它的父級則爲循環引用
        if (_parent.originalParent === obj) {
            // 循環引用直接返回同級的新對象
            return _parent.currentParent;
        }
        _parent = _parent.parent;
    }
    for (let i = 0; i < keys.length; i++) {
        key = keys[i];
        temp= obj[key];
        // 如果字段的值也是一個對象
        if (temp && typeof temp=== 'object') {
            // 遞歸執行深拷貝 將同級的待拷貝對象與新對象傳遞給 parent 方便追溯循環引用
            result[key] = DeepCopy(temp, {
                originalParent: obj,
                currentParent: result,
                parent: parent
            });

        } else {
            result[key] = temp;
        }
    }
    return result;
}

var obj1 = {
    x: 1, 
    y: 2
};
obj1.z = obj1;

var obj2 = deepCopy(obj1);
console.log(obj1); //太長了去瀏覽器試一下吧~ 
console.log(obj2); //太長了去瀏覽器試一下吧~ 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章