摘要:而引用類型值是指那些保存堆內存中的對象,意思是變量中保存的實際上只是一個指針,這個指針指向內存中的另一個位置,該位置保存對象。而堆內存主要負責對象這種變量類型的存儲。我們需要明確一點,深拷貝與淺拷貝的概念只存在于引用類型。
深拷貝和淺拷貝
說起深拷貝和淺拷貝,首先我們來看兩個栗子
// 栗子1 var a = 1,b=a; console.log(a); console.log(b) b = 2; console.log(a); console.log(b) // 栗子2 var obj1 = {x: 1, y: 2}, obj2 = obj1; console.log(obj1) //{x: 1, y: 2} console.log(obj2) //{x: 1, y: 2} obj2.x = 2; //修改obj2.x console.log(obj1) //{x: 2, y: 2} console.log(obj2) //{x: 2, y: 2}
按照慣性思維,栗子1中obj1應該跟a一樣,不會因另外一個值的改變而改變的啊,而這里卻是obj1跟著obj2的改變而改變了?同樣都是變量,怎么就表現不一樣了呢?難道存在等級上的優劣?此處需要沉思一小會。要解決這個問題,就要引入一個JS中基本類型和引用類型的概念了。
基本類型和引用類型ECMAScript變量包含兩種不同數據類型的值:基本類型值和引用類型值。基本類型值指的是那些保存在棧內存中的簡單數據段,即這種值完全保存在內存中的一個位置。而引用類型值是指那些保存堆內存中的對象,意思是變量中保存的實際上只是一個指針,這個指針指向內存中的另一個位置,該位置保存對象。兩類數據的保存方式
從上圖可以看到,棧內存主要用于存儲各種基本類型的變量,包括Boolean、Number、String、Undefined、Null等以及對象變量的指針。而堆內存主要負責對象Object這種變量類型的存儲。目前基本類型有:
Boolean、Null、Undefined、Number、String、Symbol,引用類型有:Object、Array、Function。Symbol就是ES6才出來的,之后也可能會有新的類型出來。
讓我們再回到前面的案例,栗子1中的值為基本類型,栗子2中的值為引用類型,栗子2中的賦值就是典型的淺拷貝。我們需要明確一點,深拷貝與淺拷貝的概念只存在于引用類型。
既然已經知道了深拷貝與淺拷貝的來由,那么該如何實現深拷貝?我們分別來看看Array和Object自有方法是否支持:
var arr1 = [1, 2]; var arr2 = arr1.slice(); console.log(arr1); //[1, 2] console.log(arr2); //[1, 2] arr2[0] = 3; //修改arr2 console.log(arr1); //[1, 2] console.log(arr2); //[3, 2]
此時,arr2的修改并沒有影響到arr1,看來深拷貝的實現并沒有那么難嘛。我們把arr1改成二維數組再來看看結果
var arr1 = [1, 2, [3, 4]]; var arr2 = arr1.slice(); console.log(arr1); //[1, 2, [3, 4]] console.log(arr2); //[1, 2, [3, 4]] arr2[2][1] = 5; console.log(arr1); //[1, 2, [3, 5]] console.log(arr2); //[1, 2, [3, 5]]
咦,arr2又改變了arr1,看來slice()只能實現一維數組的深拷貝,并不能實現真正的深拷貝。與之有同等特性的還有:concat、Array.from() 。
研究完Array,我們來看看Object
var obj1 = {x: 1, y: 2}; var obj2 = Object.assign({}, obj1); console.log(obj1) //{x: 1, y: 2} console.log(obj2) //{x: 1, y: 2} obj2.x = 2; //修改obj2.x console.log(obj1) //{x: 1, y: 2} console.log(obj2) //{x: 2, y: 2}
var obj1 = { x: 1, y: { m: 1 } }; var obj2 = Object.assign({}, obj1); console.log(obj1) //{x: 1, y: {m: 1}} console.log(obj2) //{x: 1, y: {m: 1}} obj2.y.m = 2; //修改obj2.y.m console.log(obj1) //{x: 1, y: {m: 2}} console.log(obj2) //{x: 2, y: {m: 2}}
經實踐證明,Object.assign()跟Array一樣也只能實現一維對象的深拷貝。造成只能實現一維對象深拷貝的原因是第一層的屬性確實實現了深拷貝,擁有了獨立的內存,但更深的屬性卻仍然公用了地址,所以才會造成上面的問題。
那怎么真正的實現引用類型的深拷貝呢?接下來要有請正主入場
1.JSON.parse(JSON.stringify(obj))
var obj1 = { x: 1, y: { m: 1 } }; var obj2 = JSON.parse(JSON.stringify(obj1)); console.log(obj1) //{x: 1, y: {m: 1}} console.log(obj2) //{x: 1, y: {m: 1}} obj2.y.m = 2; //修改obj2.y.m console.log(obj1) //{x: 1, y: {m: 1}} console.log(obj2) //{x: 2, y: {m: 2}}
JSON.parse(JSON.stringify(obj)) 簡單粗暴,簡簡單單讓你功力倍增,不過MDN文檔的描述有句話寫的很清楚:
undefined、任意的函數以及 symbol 值,在序列化過程中會被忽略(出現在非數組對象的屬性值中時)或者被轉換成 null(出現在數組中時)。詳情可以戳這里MDN文檔
var obj1 = { x: 1, y: undefined, z: function add(z1, z2) { return z1 + z2 }, a: Symbol("foo") }; var obj2 = JSON.parse(JSON.stringify(obj1)); console.log(obj1) //{x: 1, y: undefined, z: ?, a: Symbol(foo)} console.log(JSON.stringify(obj1)); //{"x":1} console.log(obj2) //{x: 1}
經實踐證明,在將obj1進行JSON.stringify()序列化的過程中,y、z、a都被忽略了,也就驗證了MDN文檔的描述。既然這樣,那JSON.parse(JSON.stringify(obj))的使用也是有局限性的,不能深拷貝含有undefined、function、symbol值的對象,不過JSON.parse(JSON.stringify(obj))簡單粗暴,已經滿足90%的使用場景了。
經過驗證,我們發現JS 提供的自有方法并不能徹底解決Array、Object的深拷貝問題。只能祭出大殺器:遞歸
2.遞歸
function deepCopy(obj) { // 創建一個新對象 let result = {} let keys = Object.keys(obj), key = null, temp = null; for (let i = 0; i < keys.length; i++) { key = keys[i]; temp = obj[key]; // 如果字段的值也是一個對象則遞歸操作 if (temp && typeof temp === "object") { result[key] = deepCopy(temp); } else { // 否則直接賦值給新對象 result[key] = temp; } } return result; } var obj1 = { x: { m: 1 }, y: undefined, z: function add(z1, z2) { return z1 + z2 }, a: Symbol("foo") }; var obj2 = deepCopy(obj1); obj2.x.m = 2; console.log(obj1); //{x: {m: 1}, y: undefined, z: ?, a: Symbol(foo)} console.log(obj2); //{x: {m: 2}, y: undefined, z: ?, a: Symbol(foo)}
可以看到,遞歸完美的解決了前面遺留的所有問題。但是,還有一個非常特殊極端的場景:循環引用拷貝
var obj1 = { x: 1, y: 2 }; obj1.z = obj1; var obj2 = deepCopy(obj1);
此時如果調用剛才的deepCopy函數的話,會陷入一個循環的遞歸過程,從而導致爆棧。解決這個問題也非常簡單,只需要判斷一個對象的字段是否引用了這個對象或這個對象的任意父級即可
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);總結
簡單的一維層次的拷貝可以利用數組自身方法和對象的Object.assign實現,在二維層次上方法失效,無法實現深拷貝
簡單粗暴的常見的拷貝可以通過JSON.parse(JSON.stringify(obj))實現,但對于屬性的某些特殊類型的值失效。
終極方法,用遞歸實現引用類型的深拷貝
當然還有其他方法,比如使用第三方庫內封裝的方法
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/102255.html
摘要:相信人很多學習的過程中都踩了深拷貝和淺拷貝的坑,深拷貝和淺拷貝的區別我就不再贅述了,今天我來寫一下我自己實現深拷貝的各種方法。中的深拷貝也是用類似方法實現。 相信人很多學習js的過程中都踩了深拷貝和淺拷貝的坑,深拷貝和淺拷貝的區別我就不再贅述了,今天我來寫一下我自己實現深拷貝的各種方法。 比較簡單的拷貝方式可以借用瀏覽器的Json對象去實現,先把對象轉化為json字符串,在解析回對...
摘要:深拷貝和淺拷貝的區別背景最近在用框架寫頁面,賦值給中的對象時會出現一個問題,賦值和被賦值對象之中任何一個有變化,另一個也會隨之變化。 深拷貝和淺拷貝的區別 背景:最近在用vue框架寫頁面,賦值給Vue.$data中的對象時會出現一個問題,賦值和被賦值對象之中任何一個有變化,另一個也會隨之變化。例如: var b = { foo: 123 }; var vm = new Vue(...
摘要:本文解釋中深拷貝和淺拷貝的區別。深拷貝深拷貝指遞歸的復制對象的屬性給新對象。有些時候一層的深拷貝被認為是淺拷貝,比如的值是一個對象,淺拷貝出來的新對象直接引用了原對象的對象,所以也會相互影響的。 本文解釋javascript中深拷貝和淺拷貝的區別。 淺拷貝/Shallow Copy 淺拷貝指拷貝了引用值。 var original = {prop1 : Prop1, prop2 : p...
摘要:關于深拷貝和淺拷貝從原理看淺拷貝拷貝一層,對象級別的則拷貝引用深拷貝拷貝多層,每個層級的屬性都會拷貝從現象看復制了,被修改后,隨變化而變化淺拷貝不變深拷貝深拷貝針對的復雜的類型數據如直接賦值的單層拷貝,如,雖然不受的影響,但是這也不算做 關于深拷貝和淺拷貝 從原理看: 淺拷貝:拷貝一層,對象級別的則拷貝引用 深拷貝:拷貝多層,每個層級的屬性都會拷貝 從現象看:A復制了B,B被修改后...
閱讀 1411·2021-10-11 10:59
閱讀 3112·2019-08-30 15:54
閱讀 2732·2019-08-30 13:19
閱讀 2461·2019-08-30 13:02
閱讀 2374·2019-08-30 10:57
閱讀 3355·2019-08-29 15:40
閱讀 985·2019-08-29 15:39
閱讀 2306·2019-08-29 12:40