實現深拷貝的幾種方式
目前使用過四種
1、遞迴遞迴去複製所有層級屬性
function deepClone(obj){ let objClone = Array.isArray(obj)?[]:{}; if(obj && typeof obj==="object"){ for(key in obj){ if(obj.hasOwnProperty(key)){ //判斷ojb子元素是否為物件,如果是,遞迴複製 if(obj[key]&&typeof obj[key] ==="object"){ objClone[key]= deepClone(obj[key]); }else{ //如果不是,簡單複製 objClone[key] = obj[key]; } } } } return objClone; } let a=[1,2,3,4], b=deepClone(a); a[0]=2; console.log(a,b);
跟之前想象的一樣,現在b脫離了a的控制,不再受a影響了。
這裡再次強調,深拷貝,是拷貝物件各個層級的屬性,可以看個例子。JQ裡有一個extend方法也可以拷貝物件,我們來看看
let a=[1,2,3,4], b=a.slice(); a[0]=2; console.log(a,b);
那是不是說slice方法也是深拷貝了,畢竟b也沒受a的影響,上面說了,深拷貝是會拷貝所有層級的屬性,還是這個例子,我們把a改改
let a=[0,1,[2,3],4], b=a.slice();
a[0]=1; a[2][0]=1; console.log(a,b);
拷貝的不徹底啊,b物件的一級屬性確實不受影響了,但是二級屬性還是沒能拷貝成功,仍然脫離不了a的控制,說明slice根本不是真正的深拷貝。
這裡引用知乎問答裡面的一張圖
第一層的屬性確實深拷貝,擁有了獨立的記憶體,但更深的屬性卻仍然公用了地址,所以才會造成上面的問題。
同理,concat方法與slice也存在這樣的情況,他們都不是真正的深拷貝,這裡需要注意。
2.除了遞迴,我們還可以借用JSON物件的parse和stringify
function deepClone(obj){ let _obj = JSON.stringify(obj), objClone = JSON.parse(_obj); return objClone } let a=[0,1,[2,3],4], b=deepClone(a); a[0]=1; a[2][0]=1; console.log(a,b);
可以看到,這下b是完全不受a的影響了。
附帶說下,JSON.stringify與JSON.parse除了實現深拷貝,還能結合localStorage實現物件陣列儲存。有興趣可以閱讀博主這篇文章。
localStorage儲存陣列,物件,localStorage,sessionStorage儲存陣列物件
3.除了上面兩種方法之外,我們還可以借用JQ的extend方法。
$.extend( [deep ], target, object1 [, objectN ] )
deep表示是否深拷貝,為true為深拷貝,為false,則為淺拷貝
targetObject型別 目標物件,其他物件的成員屬性將被附加到該物件上。
object1objectN可選。 Object型別 第一個以及第N個被合併的物件。
let a=[0,1,[2,3],4], b=$.extend(true,[],a); a[0]=1; a[2][0]=1; console.log(a,b);
可以看到,效果與上面方法一樣,只是需要依賴JQ庫。
說了這麼多,瞭解深拷貝也不僅僅是為了應付面試題,在實際開發中也是非常有用的。例如後臺返回了一堆資料,你需要對這堆資料做操作,但多人開發情況下,你是沒辦法明確這堆資料是否有其它功能也需要使用,直接修改可能會造成隱性問題,深拷貝能幫你更安全安心的去操作資料,根據實際情況來使用深拷貝,大概就是這個意思。
4.lodash的_.cloneDeep()
參考部落格:https://www.cnblogs.com/echolun/p/7889848.html
以下是我參看的一位關於深拷貝的問題解決。
JSON.parse
先將一個物件轉為json物件。然後再解析這個json物件。
let obj = {a:{b:22}};
let copy = JSON.parse(JSON.stringify(obj));
這種方法的優點就是程式碼寫起來比較簡單。但是缺點也是顯而易見的。你先是建立一個臨時的,可能很大的字串,只是為了把它重新放回解析器。另一個缺點是這種方法不能處理迴圈物件。
如下面的迴圈物件用這種方法的時候會丟擲異常
let a = {};
let b = {a};
a.b = b;
let copy = JSON.parse(JSON.stringify(a));
諸如 Map, Set, RegExp, Date, ArrayBuffer 和其他內建型別在進行序列化時會丟失。
let a = {};
let b = new Set();
b.add(11);
a.test = b;
let copy = JSON.parse(JSON.stringify(a));
a 的值列印如下
copy的值列印如下
對比發現,Set已丟失。
Structured Clone 結構化克隆演算法
MessageChannel
建立兩個端,一個端傳送訊息,另一個端接收訊息。
function structuralClone(obj) { return new Promise(resolve =>{ const {port1, port2} = new MessageChannel(); port2.onmessage = ev => resolve(ev.data); port1.postMessage(obj); }) } const obj = /* ... */; structuralClone(obj).then(res=>{ console.log(res); })
這種方法的優點就是能解決迴圈引用的問題,還支援大量的內建資料型別。缺點就是這個方法是非同步的。
History API
利用history.replaceState。這個api在做單頁面應用的路由時可以做無重新整理的改變url。這個物件使用結構化克隆,而且是同步的。但是我們需要注意,在單頁面中不要把原有的路由邏輯搞亂了。所以我們在克隆完一個物件的時候,要恢復路由的原狀。
function structuralClone(obj) { const oldState = history.state; history.replaceState(obj, document.title); const copy = history.state; history.replaceState(oldState, document.title); return copy; } var obj = {}; var b = {obj}; obj.b = b var copy = structuralClone(obj); console.log(copy);
這個方法的優點是。能解決迴圈物件的問題,也支援許多內建型別的克隆。並且是同步的。但是缺點就是有的瀏覽器對呼叫頻率有限制。比如Safari 30 秒內只允許呼叫 100 次
Notification API
這個api主要是用於桌面通知的。如果你使用Facebook的時候,你肯定會發現時常在瀏覽器的右下角有一個彈窗,對就是這傢伙。我們也可以利用這個api實現js物件的深拷貝。
function structuralClone(obj) { return new Notification('', {data: obj, silent: true}).data; } var obj = {}; var b = {obj}; obj.b = b var copy = structuralClone(obj); console.log(copy)
同樣是優點和缺點並存,優點就是可以解決迴圈物件問題,也支援許多內建型別的克隆,並且是同步的。缺點就是這個需要api的使用需要向用戶請求許可權,但是用在這裡克隆資料的時候,不經使用者授權也可以使用。在http協議的情況下會提示你再https的場景下使用。
lodash的_.cloneDeep()
支援迴圈物件,和大量的內建型別,對很多細節都處理的比較不錯。推薦使用。
支援的型別有很多
我們這裡再次關注一下lodash是如何解決迴圈應用這個問題的?
從相關的程式碼中。我們可以發現。lodash是用一個棧記錄了。所有被拷貝的引用值。如果再次碰到同樣的引用值的時候,不會再去拷貝一遍。而是利用之前已經拷貝好的值。
lodash深拷貝的詳細的原始碼可以在這裡檢視。
https://github.com/lodash/lodash/blob/master/cloneDeep.js
實現一個簡易點的深拷貝,以解決迴圈引用的問題為目標
我們僅僅實現一個簡易點的深拷貝。能優雅的處理迴圈引用的即可。在實現深拷貝之前,我們首先溫習回顧一下js中的遍歷物件的屬性的方法和各種方法的優缺點。
js中遍歷一個物件的屬性的方法
- Object.keys() 僅僅返回自身的可列舉屬性,不包括繼承來的,更不包括Symbol屬性
- Object.getOwnPropertyNames() 返回自身的可列舉和不可列舉屬性。但是不包括Symbol屬性
- Object.getOwnPropertySymbols() 返回自身的Symol屬性
- for...in 可以遍歷物件的自身的和繼承的可列舉屬性,不包含Symbol屬性
- Reflect.ownkeys() 返回物件自身的所有屬性,不管是否可列舉,也不管是否是Symbol。注意不包括繼承的屬性
實現深拷貝,解決迴圈引用問題
/** * 判斷是否是基本資料型別 * @param value */ function isPrimitive(value){ return (typeof value === 'string' || typeof value === 'number' || typeof value === 'symbol' || typeof value === 'boolean') } /** * 判斷是否是一個js物件 * @param value */ function isObject(value){ return Object.prototype.toString.call(value) === "[object Object]" } /** * 深拷貝一個值 * @param value */ function cloneDeep(value){ // 記錄被拷貝的值,避免迴圈引用的出現 let memo = {}; function baseClone(value){ let res; // 如果是基本資料型別,則直接返回 if(isPrimitive(value)){ return value; // 如果是引用資料型別,我們淺拷貝一個新值來代替原來的值 }else if(Array.isArray(value)){ res = [...value]; }else if(isObject(value)){ res = {...value}; } // 檢測我們淺拷貝的這個物件的屬性值有沒有是引用資料型別。如果是,則遞迴拷貝 Reflect.ownKeys(res).forEach(key=>{ if(typeof res[key] === "object" && res[key]!== null){ //此處我們用memo來記錄已經被拷貝過的引用地址。以此來解決迴圈引用的問題 if(memo[res[key]]){ res[key] = memo[res[key]]; }else{ memo[res[key]] = res[key]; res[key] = baseClone(res[key]) } } }) return res; } return baseClone(value) }
驗證我們寫的cloneDeep是否能解決迴圈應用的問題
var obj = {}; var b = {obj}; obj.b = b var copy = cloneDeep(obj); console.log(copy);
完美。大功告成
我們雖然的確解決了深拷貝的大部分問題。不過很多細節還沒有去處理。在生產環境,我們還是要使用lodash的cloneDeep。cloneDeep對每個資料型別都單獨處理的非常好。比如ArrayBuffer什麼的。我們都沒有處理。
作者:Jesse
連結:https://www.zhihu.com/question/23031215/answer/460652947
來源:知乎
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。
參考部落格:https://www.zhihu.com/question/23031215(講的比較好,推薦)