在說深拷貝與淺拷貝前,咱們先看兩個簡單的案例:javascript
//案例1 var num1 = 1, num2 = num1; console.log(num1) //1 console.log(num2) //1 num2 = 2; //修改num2 console.log(num1) //1 console.log(num2) //2 //案例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}
按照常規思惟,obj1
應該和num1
同樣,不會由於另一個值的改變而改變,而這裏的obj1
卻隨着obj2
的改變而改變了。一樣是變量,爲何表現不同呢?這就要引入JS中基本類型和引用類型的概念了。java
ECMAScript變量可能包含兩種不一樣數據類型的值:基本類型值和引用類型值。基本類型值指的是那些保存在棧內存中的簡單數據段,即這種值徹底保存在內存中的一個位置。而引用類型值是指那些保存堆內存中的對象,意思是變量中保存的實際上只是一個指針,這個指針指向內存中的另外一個位置,該位置保存對象。
打個比方,基本類型和引用類型在賦值上的區別能夠按「連鎖店」和「單店」來理解:基本類型賦值等於在一個新的地方安裝連鎖店的規範標準新開一個分店,新開的店與其餘舊店互不相關,各自運營;而引用類型賦值至關於一個店有兩把鑰匙,交給兩個老闆同時管理,兩個老闆的行爲都有可能對一間店的運營形成影響。jquery
上面清晰明瞭的介紹了基本類型和引用類型的定義和區別。目前基本類型有:Boolean、Null、Undefined、Number、String、Symbol,引用類型有:Object、Array、Function。之因此說「目前」,由於Symbol就是ES6纔出來的,以後也可能會有新的類型出來。git
再回到前面的案例,案例1中的值爲基本類型,案例2中的值爲引用類型。案例2中的賦值就是典型的淺拷貝,而且深拷貝與淺拷貝的概念只存在於引用類型。github
既然已經知道了深拷貝與淺拷貝的來由,那麼該如何實現深拷貝?咱們先分別看看Array和Object自有方法是否支持:segmentfault
var arr1 = [1, 2], 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]], 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() 。函數
var obj1 = {x: 1, y: 2}, 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()也只能實現一維對象的深拷貝。post
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
(出如今數組中時)。
咱們再來把obj1
改造下:
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的深拷貝問題。只能祭出大殺器:遞歸
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)}
能夠看到,遞歸完美的解決了前面遺留的全部問題,咱們也能夠用第三方庫:jquery的$.extend
和lodash的_.cloneDeep
來解決深拷貝。上面雖然是用Object驗證,但對於Array也一樣適用,由於Array也是特殊的Object。
到這裏,深拷貝問題基本能夠告一段落了。可是,還有一個很是特殊的場景:
循環引用拷貝
var obj1 = { x: 1, y: 2 }; obj1.z = obj1; var obj2 = deepCopy(obj1);
此時若是調用剛纔的deepCopy函數的話,會陷入一個循環的遞歸過程,從而致使爆棧。jquery的$.extend
也沒有解決。解決這個問題也很是簡單,只須要判斷一個對象的字段是否引用了這個對象或這個對象的任意父級便可,修改一下代碼:
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); //太長了去瀏覽器試一下吧~
至此,已完成一個支持循環引用的深拷貝函數。固然,也可使用lodash的_.cloneDeep
噢~。
文章同步於如下社區,能夠選一個關注我噢 。◕‿◕。