面試題目:如何實現對一個數組或對象的淺拷貝和深拷貝?
WTF,複製還分兩種,第一次遇到這種問題的時候很是無語呢,先來看看一般的答案的理解。
淺拷貝是只拷貝一層,深層次的對象級別就只拷貝引用。 深拷貝是拷貝多層,每一級別的數據都拷貝出來。也就是說,基本數據類型其實不存在深淺拷貝的問題,只有對象和數組才存在深淺拷貝的問題。
主要解決的是什麼問題呢?你去買房子,看中一套不錯要了,然後中介給你列印了一份合同,你籤字付錢。過一段時間去看,哎呀我去,怎麼裝修了?另外一個人也拿著同樣有合同、付款憑證。我以為是我買的房子,結果中介一房兩賣,別人也能搞。這怎麼行?
JS數據類型
js分為基本數據類型和複雜數據類型。
基本數據類型包括:String、Number、Boolean、Null、Undefined
複雜(引用)類型包括:Object、Array、Function
在開發過程中,經常使用 typeof 來檢測數據類型。默認var聲明的時候,如果不進行賦值,類型就是undefined。布爾值是boolean只有 true,false兩種值。
聲明的時候用的null,這時候代表空對象,使用typeof檢測的時候,顯示是Object。
JS內存管理
JS代碼運行的時候,數據都要寫入內存進行調用的,而不同的數據類型在內存中存放的方式是不一樣的。
基本數據類型是存儲在棧數據空間中,複雜數據類型是存儲在堆數據空間中的,而對數據空間不能直接訪問,需要棧這邊進行位置指引。
一個不是很恰當的比喻就是內存相當於倉庫。
倉庫裡面分了兩個區域,一邊是都是小格子,另一邊都是大貨櫃。簡單數據類型比如你的一本書,你的一份帳單什麼的就直接放在小格子裡面就好了。
另外你有一屋子書和一屋子的帳單,小格子放不下。你就租了一個小格子和一個倉庫。小格子裡面放著倉庫的鑰匙和倉庫的位置,倉庫裡面放東西。
實際的內存讀取方式也類似。要找自己的小格子,你就要從上到下挨著找。想要找自己貨櫃裡面的東西,還是需要先去小格子裡面找到存放鑰匙和位置的格子,找到以後直接去找貨櫃。
下圖是
淺拷貝和深拷貝
再沒有了解到深拷貝和淺拷貝知識的時候,一般拷貝就是從新賦值。聲明個數據直接用另外一個對象賦值。這個就算是淺拷貝。
當遇到複雜對象的時候,複製的只是對象的指針,並沒有重新開闢大的空間進行複製。這時候造成的影響就是對兩個指針進行數據操作的時候,操作的是同一個數據內容,相互之間是受影響的。
而深拷貝就是需要連指針到內容都進行複製,兩個指針指向兩個空間的內容。各自操作已經不受影響。
淺拷貝的實現方式
淺拷貝的複製就是直接複製賦值就可以了。
方法一:
function simpleClone(initalObj) { var obj = {}; for ( var i in initalObj) { obj[i] = initalObj[i]; } return obj;}var obj = { b:{ a: "world", b: 21 }, c:["Bob", "Tom", "Jenny"], d:function() { alert("hello world"); }}var cloneObj = simpleClone(obj); console.log(cloneObj.b); console.log(cloneObj.c);console.log(cloneObj.d);cloneObj.b.a = "changed";cloneObj.c = [1, 2, 3];cloneObj.d = function() { alert("changed"); };console.log(obj.b);console.log(obj.c);console.log(obj.d);自行運行查看下變化及原因。
方法二: Object.assign是ES6的新函數。Object.assign() 方法可以把任意多個的源對象自身的可枚舉屬性拷貝給目標對象,然後返回目標對象。但是 Object.assign() 進行的是淺拷貝,拷貝的是對象的屬性的引用,而不是對象本身。
Object.assign(target, ...sources)var obj = { a: {a: "hello", b: 21} };var initalObj = Object.assign({}, obj);initalObj.a.a = "changed";console.log(obj.a.a); // "changed"需要注意的是:
Object.assign()可以處理一層的深度拷貝,如下:
var obj1 = { a: 10, b: 20, c: 30 };var obj2 = Object.assign({}, obj1);obj2.b = 100;console.log(obj1);// { a: 10, b: 20, c: 30 } <-- 沒被改到console.log(obj2);// { a: 10, b: 100, c: 30 }深拷貝的實現方式
如果要複製的對象只有一層,對象裡面的元素全是基本元素的話,前面的淺拷貝案例其實就完成了深拷貝的功能。我們重點說一下多層對象。
1.通過JSON轉換 用JSON.stringify把對象轉成字符串,再用JSON.parse把字符串轉成新的對象。
但是這種方法也有不少壞處,譬如它會拋棄對象的constructor。也就是深拷貝之後,不管這個對象原來的構造函數是什麼,在深拷貝之後都會變成Object。並且只能處理常見的Number, String, Boolean, Array, 扁平對象等這些能被JSON表示的數據結構。RegExp對象是無法通過這種方式深拷貝。
2.遞歸拷貝
function deepClone(initalObj, finalObj) { var obj = finalObj || {}; for (var i in initalObj) { var prop = initalObj[i]; if(prop === obj) { continue; } if (typeof prop === 'object') { obj[i] = (prop.constructor === Array) ? [] : {}; arguments.callee(prop, obj[i]); } else { obj[i] = prop; } } return obj;}遞歸拷貝就是將對象逐層解開進行剖析,逐層新建對象,逐層複製,知道最深處的所有簡單數據都複製上。
但是要注意要注意對象引用對象的情況,會掉入死循環。
3.使用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;}小案例
之前在進行公司VUE項目開發的過程中,由於需要將富文本編輯器抽離成為一個單獨的組件,然後將內容的對象傳入進去。如果按照傳統的vue組件開發的流程,肯定是要接收傳入、賦值給本組件、本組件編輯器修改、修改完畢的內容再進行emit外傳,然後組件外部接受,進一步處理。
但是由於vue組件之間淺拷貝的特性,其實傳入的對象修改之後,外部組件直接取值拿到的就是最新的值。
也是因為這個發現才對深拷貝和淺拷貝有了更加深入的了解。