前言: 最開始意識到深拷貝的重要性是在我使用redux的時候(react + redux), redux的機制要求在reducer中必須返回一個新的對象,而不能對原來的對象做改動,事實上,當時我當然不會主動犯這個錯誤,但很多時候,一不小心可能就會修改了原來的對象,例如:var newObj = obj; newObj.xxx = xxx 實際上,這個時候newObj和obj兩個引用指向的是同一個對象,我修改了newObj,實際上也就等同於修改了obj,這,就是我和深淺拷貝的第一次相遇。
淺談深拷貝和淺拷貝
深拷貝和淺拷貝的區別
1.淺拷貝: 將原對象或原數組的引用直接賦給新對象,新數組,新對象/數組只是原對象的一個引用
2.深拷貝: 創建一個新的對象和數組,將原對象的各項屬性的“值”(數組的所有元素)拷貝過來,是“值”而不是“引用”
為什么要使用深拷貝?
我們希望在改變新的數組(對象)的時候,不改變原數組(對象)
深拷貝的要求程度
我們在使用深拷貝的時候,一定要弄清楚我們對深拷貝的要求程度:是僅“深”拷貝第一層級的對象屬性或數組元素,還是遞歸拷貝所有層級的對象屬性和數組元素?
怎么檢驗深拷貝成功
改變任意一個新對象/數組中的屬性/元素, 都不改變原對象/數組
只對第一層級做拷貝
深拷貝數組(只拷貝第一級數組元素)
1.直接遍歷
var array = [1, 2, 3, 4]; function copy (array) { let newArray = [] for(let item of array) { newArray.push(item); } return newArray; } var copyArray = copy(array); copyArray[0] = 100; console.log(array); // [1, 2, 3, 4] console.log(copyArray); // [100, 2, 3, 4]
該方法不做解釋(逃...)
2. slice()
var array = [1, 2, 3, 4]; var copyArray = array.slice(); copyArray[0] = 100; console.log(array); // [1, 2, 3, 4] console.log(copyArray); // [100, 2, 3, 4]
slice() 方法返回一個從已有的數組中截取一部分元素片段組成的新數組(不改變原來的數組!)
用法:array.slice(start,end) start表示是起始元素的下標, end表示的是終止元素的下標
當slice()不帶任何參數的時候,默認返回一個長度和原數組相同的新數組
3. concat()
var array = [1, 2, 3, 4]; var copyArray = array.concat(); copyArray[0] = 100; console.log(array); // [1, 2, 3, 4] console.log(copyArray); // [100, 2, 3, 4]
concat() 方法用於連接兩個或多個數組。( 該方法不會改變現有的數組,而僅僅會返回被連接數組的一個副本。)
用法:array.concat(array1,array2,......,arrayN)
因為我們上面調用concat的時候沒有帶上參數,所以var copyArray = array.concat();實際上相當於var copyArray = array.concat([]);
也即把返回數組和一個空數組合並后返回
但是,事情當然不會這么簡單,我上面的標題是 “深拷貝數組(只拷貝第一級數組元素)”,這里說的意思是對於一級數組元素是基本類型變量(如number,String,boolean)的簡單數組, 上面這三種拷貝方式都能成功,但對第一級數組元素是對象或者數組等引用類型變量的數組,上面的三種方式都將失效,例如:
var array = [ { number: 1 }, { number: 2 }, { number: 3 } ]; var copyArray = array.slice(); copyArray[0].number = 100; console.log(array); // [{number: 100}, { number: 2 }, { number: 3 }] console.log(copyArray); // [{number: 100}, { number: 2 }, { number: 3 }]
深拷貝對象
1.直接遍歷
var obj = { name: '彭湖灣', job: '學生' } function copy (obj) { let newObj = {}; for (let item in obj ){ newObj[item] = obj } return newObj; } var copyObj = copy(obj); copyObj.name = '我才不是彭湖灣呢! 哼 (。・`ω´・)'; console.log(obj); // {name: "彭湖灣", job: "學生"} console.log(copyObj); // {name: "我才不是彭湖灣呢! 哼 (。・`ω´・)", job: Object}
該方法不做解釋(逃...)
2.ES6的Object.assign
var obj = { name: '彭湖灣', job: '學生' } var copyObj = Object.assign({}, obj); copyObj.name = '我才不叫彭湖灣呢! 哼 (。・`ω´・)'; console.log(obj); // {name: "彭湖灣", job: "學生"} console.log(copyObj); // {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(obj.name) // 彭湖灣 console.log(copyObj.name) // 我才不叫彭湖灣呢! 哼 (。・`ω´・)
擴展運算符(...)用於取出參數對象的所有可遍歷屬性,拷貝到當前對象之中
對多層嵌套對象,很遺憾,上面三種方法,都會失敗:
var obj = { name: { firstName: '彭', lastName: '湖灣' }, job: '學生' } var copyObj = Object.assign({}, obj) copyObj.name.lastName = '湖水的小淺灣'; console.log(obj.name.lastName); // 湖水的小淺灣 console.log(copyObj.name.lastName); // 湖水的小淺灣
拷貝所有層級
有沒有更強大一些的解決方案呢?使得我們能夠
1.不僅拷貝第一層級,還能夠拷貝數組或對象所有層級的各項值
2. 不是單獨針對數組或對象,而是能夠通用於數組,對象和其他復雜的JSON形式的對象
請看下面:
下面這一招可謂是“一招鮮,吃遍天”
1.JSON.parse(JSON.stringify(XXXX))
var array = [ { number: 1 }, { number: 2 }, { number: 3 } ]; var copyArray = JSON.parse(JSON.stringify(array)) copyArray[0].number = 100; console.log(array); // [{number: 1}, { number: 2 }, { number: 3 }] console.log(copyArray); // [{number: 100}, { number: 2 }, { number: 3 }]
能用大招殺的就不要用q殺嘛!!
2.手動寫遞歸
你說啥? 你說上面的那種方法太無腦, 一定要自己寫一段遞歸才有做技術的感覺? OK成全你!
var array = [ { number: 1 }, { number: 2 }, { number: 3 } ]; function copy (obj) { var newobj = obj.constructor === Array ? [] : {}; if(typeof obj !== 'object'){ return; } for(var i in obj){ newobj[i] = typeof obj[i] === 'object' ? copy(obj[i]) : obj[i]; } return newobj } var copyArray = copy(array) copyArray[0].number = 100; console.log(array); // [{number: 1}, { number: 2 }, { number: 3 }] console.log(copyArray); // [{number: 100}, { number: 2 }, { number: 3 }]
【注意】:上文的所有的示例都忽略了一些特殊的情況: 對對象/數組中的Function,正則表達式等特殊類型的拷貝
存在大量深拷貝需求的代碼——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
參考資料:
知乎《 javascript中的深拷貝和淺拷貝?》 https://www.zhihu.com/question/23031215
阮一峰 《ECMASript6入門》 http://es6.ruanyifeng.com/
MDN javascript 數組API https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array
另外: 除了以上參考資料,文中的array.slice(0)和obj.concat()實現數組拷貝的方法實際上參考了網路上的一些文章,比如博客園,CSDN和腳本之家,但由於這些文章內容相似,這里就不列出其中來源了
