詳解javaScript的深拷貝
目錄
-
淺談深拷貝和淺拷貝
-
深拷貝和淺拷貝的區別
-
為什麼要使用深拷貝?
-
深拷貝的要求程度
-
怎麼檢驗深拷貝成功
-
-
只對第一層級做拷貝
-
拷貝所有層級
-
存在大量深拷貝需求的程式碼——immutable提供的解決方案
正文
前言: 最開始意識到深拷貝的重要性是在我使用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') // 2map2.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和指令碼之家,但由於這些文章內容相似,這裡就不列出其中來源了