終於弄清楚JS的深拷貝和淺拷貝了[轉]

今天在看js的深淺拷貝的時候,發現一個很好的帖子,神貼,來自

作者:一隻努力的程序媛

爲了節省時間,我就直接先複製過來啦,致敬致敬!!!


今天,CVTE面試官問了深拷貝和淺拷貝的問題

我的回答是:淺拷貝是拷貝了對象的引用,當原對象發生變化的時候,拷貝對象也跟着變化;深拷貝是另外申請了一塊內存,內容和原對象一樣,更改原對象,拷貝對象不會發生變化;

但是面試官給我說:淺拷貝是拷貝一層,深層次的對象級別的就拷貝引用;深拷貝是拷貝多層,每一級別的數據都會拷貝出來;

回來查了一下資料,並沒有發現面試官說的這種關於深拷貝淺拷貝的說法,看了幾篇文章,終於理解了

其實總結來看,淺拷貝的時候如果數據是基本數據類型,那麼就如同直接賦值那種,會拷貝其本身,如果除了基本數據類型之外還有一層對象,那麼對於淺拷貝而言就只能拷貝其引用,對象的改變會反應到拷貝對象上;但是深拷貝就會拷貝多層,即使是嵌套了對象,也會都拷貝出來。

\color{red}{實現淺拷貝的第一種方法}

function simpleClone(initalObj) {
    var obj = {};
    for ( var i in initalObj) {
        obj[i] = initalObj[i];
    }
    return obj;
}

var obj = {
    a: "hello",
    b:{
        a: "world",
        b: 21
    },
    c:["Bob", "Tom", "Jenny"],
    d:function() {
        alert("hello world");
    }
};
var cloneObj = simpleClone(obj);
console.log(cloneObj.a);
console.log(cloneObj.b);
console.log(cloneObj.c);
console.log(cloneObj.d);

//更改原對象中的a,b,c,d,看看拷貝過來的對象是否變化
cloneObj.a = "changed";
cloneObj.b.a = "changed";
cloneObj.b.b = 25;
cloneObj.c = [1, 2, 3];
cloneObj.d = function() { alert("changed"); };
console.log(obj.a);    //hello
console.log(obj.b);    //{a:"changed",b:25},事實上就是隻有對象是拷貝的引用類型
console.log(obj.c);    //['Bob','Tom','Jenny']
console.log(obj.d);    //...alert("hello world")

事實證明面試官說的是對的,淺拷貝就是拷貝了一層,除了對象是拷貝的引用類型,其他都是直接將值傳遞,有自己的內存空間的。

\color{red}{實現淺拷貝的第二種方法}

ES6中的Object.assign方法,Object.assign是ES6的新函數。Object.assign() 方法可以把任意多個的源對象自身的可枚舉屬性拷貝給目標對象,然後返回目標對象。但是 Object.assign() 進行的是淺拷貝,拷貝的是對象的屬性的引用,而不是對象本身。

Object.assign(target, …sources)

參數:

  • target:目標對象。
  • sources:任意多個源對象。
  • 返回值:目標對象會被返回。
var obj1 = {
    a: "hello",
    b: {
        a: "hello",
        b: 21}
};

var cloneObj1= Object.assign({}, obj1);
cloneObj1.a = "changed";
cloneObj1.b.a = "changed";
console.log(obj1.a);  //hello
console.log(obj.b.a); // "changed"

另外,(這裏其實沒太看懂),爲什麼上面的改了,這裏卻沒有改。。。。說是可以拷貝深一層,如果對象只有一層的話,可以使用這個函數作爲深拷貝的方法

var obj2 = { a: 10, b: 20, c: 30 };
var cloneObj2 = Object.assign({}, obj2);
cloneObj2.b = 100;
console.log(obj2);
// { a: 10, b: 20, c: 30 } <-- 沒被改到
console.log(cloneObj2);
// { a: 10, b: 100, c: 30 }

\color{violet}{深拷貝的實現方式}

1、手動複製

把一個對象的屬性複製給另一個對象的屬性

var obj1 = { a: 10, b: 20, c: 30 };
var obj2 = { a: obj1.a, b: obj1.b, c: obj1.c };
obj2.b = 100;
console.log(obj1);
// { a: 10, b: 20, c: 30 } <-- 沒被改到
console.log(obj2);
// { a: 10, b: 100, c: 30 }

但這樣很麻煩,要一個一個自己複製;而且這樣的本質也不能算是 Deep Copy,因爲對象裏面也可能回事對象,如像下面這個狀況:

var obj1 = { body: { a: 10 } };
var obj2 = { body: obj1.body };
obj2.body.a = 20;
console.log(obj1);
// { body: { a: 20 } } <-- 被改到了
console.log(obj2);
// { body: { a: 20 } }
console.log(obj1 === obj2);
// false
console.log(obj1.body === obj2.body);
// true

雖然obj1跟obj2是不同對象,但他們會共享同一個obj1.body,所以修改obj2.body.a時也會修改到舊的。

2、對象只有一層的話可以使用上面的:Object.assign()函數

Object.assign({}, obj1)的意思是先建立一個空對象{},接着把obj1中所有的屬性複製過去,所以obj2會長得跟obj1一樣,這時候再修改obj2.b也不會影響obj1。

因爲Object.assign跟我們手動複製的效果相同,所以一樣只能處理深度只有一層的對象,沒辦法做到真正的 Deep Copy。不過如果要複製的對象只有一層的話可以考慮使用它。

3、轉成 JSON 再轉回來

用JSON.stringify把對象轉成字符串,再用JSON.parse把字符串轉成新的對象。

var obj1 = { body: { a: 10 } };
var obj2 = JSON.parse(JSON.stringify(obj1));
obj2.body.a = 20;
console.log(obj1);
// { body: { a: 10 } } <-- 沒被改到
console.log(obj2);
// { body: { a: 20 } }
console.log(obj1 === obj2);
// false
console.log(obj1.body === obj2.body);
// false

這樣做是真正的Deep Copy,這種方法簡單易用。

但是這種方法也有不少壞處,譬如它會拋棄對象的constructor。也就是深拷貝之後,不管這個對象原來的構造函數是什麼,在深拷貝之後都會變成Object。

這種方法能正確處理的對象只有 Number, String, Boolean, Array, 扁平對象,即那些能夠被 json 直接表示的數據結構。RegExp對象是無法通過這種方式深拷貝。

也就是說,只有可以轉成JSON格式的對象纔可以這樣用,像function沒辦法轉成JSON。

var obj1 = { fun: function(){ console.log(123) } };
var obj2 = JSON.parse(JSON.stringify(obj1));
console.log(typeof obj1.fun);
// 'function'
console.log(typeof obj2.fun);
// 'undefined' <-- 沒複製

要複製的function會直接消失,所以這個方法只能用在單純只有數據的對象。

4、遞歸拷貝

function deepClone(initalObj, finalObj) {    
  var obj = finalObj || {};    
  for (var i in initalObj) {        
    if (typeof initalObj[i] === 'object') {
      obj[i] = (initalObj[i].constructor === Array) ? [] : {};            
      arguments.callee(initalObj[i], obj[i]);
    } else {
      obj[i] = initalObj[i];
    }
  }    
  return obj;
}
var str = {};
var obj = { a: {a: "hello", b: 21} };
deepClone(obj, str);
console.log(str.a);

上述代碼確實可以實現深拷貝。但是當遇到兩個互相引用的對象,會出現死循環的情況。

爲了避免相互引用的對象導致死循環的情況,則應該在遍歷的時候判斷是否相互引用對象,如果是則退出循環。

改進版代碼如下:

function deepClone(initalObj, finalObj) {    
  var obj = finalObj || {};    
  for (var i in initalObj) {        
    var prop = initalObj[i];        // 避免相互引用對象導致死循環,如initalObj.a = initalObj的情況
    if(prop === obj) {            
      continue;
    }        
    if (typeof prop === 'object') {
      obj[i] = (prop.constructor === Array) ? [] : {};            
      arguments.callee(prop, obj[i]);
    } else {
      obj[i] = prop;
    }
  }    
  return obj;
}
var str = {};
var obj = { a: {a: "hello", b: 21} };
deepClone(obj, str);
console.log(str.a);

5、使用Object.create()方法

直接使用var newObj = Object.create(oldObj),可以達到深拷貝的效果。

function deepClone(initalObj, finalObj) {    
  var obj = finalObj || {};    
  for (var i in initalObj) {        
    var prop = initalObj[i];        // 避免相互引用對象導致死循環,如initalObj.a = initalObj的情況
    if(prop === obj) {            
      continue;
    }        
    if (typeof prop === 'object') {
      obj[i] = (prop.constructor === Array) ? [] : Object.create(prop);
    } else {
      obj[i] = prop;
    }
  }    
  return obj;
}

在這裏引出幾個問題:

  • var a = 1, b = 1; a == b 爲 true
  • var c = {}, d = {}; c == d 爲 false
  • var e = [], f = []; e == f 爲 false

上述帖子的根本點在哪?爲啥有的值可以直接比較,而對象和數組不行等等,我下篇文章會詳說!

傳送門—深淺拷貝的核心—棧(stack)和堆(heap)

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