js深拷貝是一件看起來很簡單的事情,但其實一點兒也不簡單。對於循環引用的問題還有一些內置數據類型的拷貝,如Map, Set, RegExp, Date, ArrayBuffer 和其餘內置類型。處理起來並不是像想象的那麼簡單。下面終結一下在實際的項目中,使用的一些深拷貝的方法的優缺點。你們也能夠 關注個人GitHub,互相交流學習進步。
先將一個對象轉爲json對象。而後再解析這個json對象。git
let obj = {a:{b:22}}; let copy = JSON.parse(JSON.stringify(obj));
這種方法的優勢就是代碼寫起來比較簡單。可是缺點也是顯而易見的。你先是建立一個臨時的,可能很大的字符串,只是爲了把它從新放回解析器。另外一個缺點是這種方法不能處理循環對象。github
以下面的循環對象用這種方法的時候會拋出異常算法
let a = {}; let b = {a}; a.b = b; let copy = JSON.parse(JSON.stringify(a));
諸如 Map, Set, RegExp, Date, ArrayBuffer 和其餘內置類型在進行序列化時會丟失。json
let a = {}; let b = new Set(); b.add(11); a.test = b; let copy = JSON.parse(JSON.stringify(a));
a 的值打印以下api
copy的值打印以下瀏覽器
對比發現,Set已丟失。異步
創建兩個端,一個端發送消息,另外一個端接收消息。post
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.replaceState。這個api在作單頁面應用的路由時能夠作無刷新的改變url。這個對象使用結構化克隆,並且是同步的。可是咱們須要注意,在單頁面中不要把原有的路由邏輯搞亂了。因此咱們在克隆完一個對象的時候,要恢復路由的原狀。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 次
這個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是如何解決循環應用這個問題的?
從相關的代碼中。咱們能夠發現。lodash是用一個棧記錄了。全部被拷貝的引用值。若是再次碰到一樣的引用值的時候,不會再去拷貝一遍。而是利用以前已經拷貝好的值。
lodash深拷貝的詳細的源碼能夠在這裏查看。
https://github.com/lodash/lod...
咱們僅僅實現一個簡易點的深拷貝。能優雅的處理循環引用的便可。在實現深拷貝以前,咱們首先溫習回顧一下js中的遍歷對象的屬性的方法和各類方法的優缺點。
/** * 判斷是不是基本數據類型 * @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什麼的。咱們都沒有處理。