深拷貝和淺拷貝


定義深拷貝、淺拷貝

不要相信其餘人說的什麼拷貝一層的是淺拷貝,若是拷貝後不影響原來對象內容的就是深拷貝,若是影響就是淺拷貝html

深拷貝通常不考慮基本數據類型(除了Object以外的),由於基本數據類型的簡單的賦值就是深拷貝,因此在說深拷貝的時候只說引用數據類型jquery

下面說下引用數據類型的淺拷貝,引用數據類型的賦值就是棧內存地址的拷貝,a 和 b 中的棧使用的是相同的內存地址,指向同一個堆內存中的內容json

那麼該如何實現深拷貝呢?(箭頭表明指針,"child"表示子對象)就是拷貝一個對象使用不一樣的棧地址開闢新的堆內存,可是堆內存中保存的內容是同樣的就實現了深拷貝數組


深拷貝和淺拷貝的區別

1.淺拷貝: 將原對象或原數組的引用直接賦給新對象,新數組,新對象/數組只是原對象的一個引用bash

2.深拷貝: 建立一個新的對象和數組,將原對象的各項屬性的「值」(數組的全部元素)拷貝過來,是「值」而不是「引用」數據結構

爲何要使用深拷貝?

咱們但願在改變新的數組(對象)的時候,不改變原數組(對象)函數

深拷貝的要求程度

咱們在使用深拷貝的時候,必定要弄清楚咱們對深拷貝的要求程度:是僅「深」拷貝第一層級的對象屬性或數組元素,仍是遞歸拷貝全部層級的對象屬性和數組元素?性能

怎麼檢驗深拷貝成功

改變任意一個新對象/數組中的屬性/元素, 都不改變原對象/數組ui

只作第一層深拷貝

深拷貝數組(只拷貝第一級數組元素) 

1.直接遍歷


var arr = [1,2,3,4];


function copy(arg){
  
  var newArr = [];
  
  for(var i = 0; i < arr.length; i++) {
    newArr.push(arr[i]);
  }
  
  return newArr;
}

var newArry = copy(arr);
console.log(newArry);
newArry[0] = 10;
console.log(newArry); // [10,2,3,4]
console.log(arr)  // [1,2,3,4]

複製代碼

2.slice()

var arr = [1,2,3,4]
var copyArr = arr.slice();
copyArr[0] = 10;
console.log(copyArr); // [10,2,3,4]
console.log(arr); // [1,2,3,4]
複製代碼

slice() 方法返回一個從已有的數組中截取一部分元素片斷組成的新數組(不改變原來的數組!) 用法:array.slice(start,end) start表示是起始元素的下標, end表示的是終止元素的下標spa

當slice()不帶任何參數的時候,默認返回一個長度和原數組相同的新數組


3.concat()

var arr = [1,2,3,4]
var copyArr = arr.concat();
copyArr[0] = 10;
console.log(copyArr); // [10,2,3,4]
console.log(arr); // [1,2,3,4]
複製代碼

concat() 方法用於鏈接兩個或多個數組。( 該方法不會改變現有的數組,而僅僅會返回被鏈接數組的一個副本。)

用法:array.concat(array1,array2,......,arrayN)

由於咱們上面調用concat的時候沒有帶上參數,因此var copyArray = array.concat();實際上至關於var copyArray = array.concat([]); 也即把返回數組和一個空數組合並後返回

可是,事情固然不會這麼簡單,我上面的標題是 「深拷貝數組(只拷貝第一級數組元素)」,這裏說的意思是對於一級數組元素是基本類型變量(如number,String,boolean)的簡單數組, 上面這三種拷貝方式都能成功,但對第一級數組元素是對象或者數組等引用類型變量的數組,上面的三種方式都將失效,例如:

var arr = [
  {number:1},
  {number:2},
  {number:3}
  
]
var copyArr = arr.slice();
copyArr[0].number = 10;
console.log(copyArr);  // [{number: 100}, { number: 2 },{ number: 3 }]
console.log(arr); // [{number: 100}, { number: 2 }, { number: 3 }]
複製代碼

深拷貝對象

1.直接遍歷

var obj = {
    name: "張三",
    job: "學生"
  }
  
  function copy (arg) {
    let newobj = {}
    for(let item in obj) {
      newobj[item] = obj;
    }
    return newobj;
  }
  
  var copyobj = copy(obj)
  copyobj.name = "李四"
  console.log(copyobj) // {name: '李四', job:: '學生'}
  console.log(obj) // {name: '張三', job:: '學生'}
複製代碼

2.ES6的Object.assign

var obj = {
  name: '張三',
  job: '學生'
}

var copyobj = Object.assign({},obj)
copyobj.name = '李四'
console.log(copyobj) // {name: '李四', job:: '學生'}
console.log(obj)    // {name: '張三', job:: '學生'}
複製代碼

Object.assign:用於對象的合併,將源對象(source)的全部可枚舉屬性,複製到目標對象(target),並返回合併後的target

用法: Object.assign(target, source1, source2); 因此 copyObj = Object.assign({}, obj); 這段代碼將會把obj中的一級屬性都拷貝到 {}中,而後將其返回賦給copyObj


3.ES6擴展運算符:

var obj = {
  name: '張三',
  job: '學生'
}

var copyobj = {...obj}
copyobj.name = '李四'
console.log(copyobj)
console.log(obj)
複製代碼

擴展運算符(...)用於取出參數對象的全部可遍歷屬性,拷貝到當前對象之中

對多層嵌套對象,很遺憾,上面三種方法,都會失敗:

var obj = {
  name: {
    firstname: '張',
    lastname: '三'
  },
  job: '學生'
}

var copyobj = Object.assign({},obj)
copyobj.name.firstname = '王'
console.log(copyobj.name.firstname) // 王
console.log(obj.name.firstname)     // 王
複製代碼

拷貝全部層級

有沒有更強大一些的解決方案呢?使得咱們可以

1.不只拷貝第一層級,還可以拷貝數組或對象全部層級的各項值 2. 不是單獨針對數組或對象,而是可以通用於數組,對象和其餘複雜的JSON形式的對象

請看下面:

一、手動複製

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

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時也會修改到舊的。
複製代碼

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

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

由於Object.assign跟咱們手動複製的效果相同,因此同樣只能處理深度只有一層的對象,沒辦法作到真正的 Deep Copy。不過若是要複製的對象只有一層的話能夠考慮使用它。
複製代碼

三、轉成 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會直接消失,因此這個方法只能用在單純只有數據的對象。

複製代碼

四、遞歸拷貝

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);
複製代碼

五、使用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;
}
複製代碼

六、jquery

jquery 有提供一個$.extend能夠用來作 Deep Copy。

var $ = require('jquery');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f);
// false
複製代碼

七、lodash

另一個很熱門的函數庫lodash,也有提供_.cloneDeep用來作 Deep Copy。

var _ = require('lodash');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);
// false
複製代碼

這個性能還不錯,使用起來也很簡單。

存在大量深拷貝需求的代碼——immutable提供的解決方案

實際上,即便咱們知道了如何在各類狀況下進行深拷貝,咱們也仍然面臨一些問題: 深拷貝其實是很消耗性能的。(咱們可能只是但願改變新數組裏的其中一個元素的時候不影響原數組,但卻被迫要把整個原數組都拷貝一遍,這不是一種浪費嗎?)因此,當你的項目裏有大量深拷貝需求的時候,性能就可能造成了一個制約的瓶頸了。

immutable的做用: 經過immutable引入的一套API,實現:

1.在改變新的數組(對象)的時候,不改變原數組(對象) 2.在大量深拷貝操做中顯著地減小性能消耗

先睹爲快:

const { Map } = require('immutable')
const map1 = Map({ a: 1, b: 2, c: 3 })
const map2 = map1.set('b', 50)
map1.get('b') // 2
map2.get('b') // 50
複製代碼

參考地址1

參考地址2

相關文章
相關標籤/搜索