因爲 js 的傳參方式有時會遇到這樣的場景:html
function setTime(data) { let result = {}; result.obj = data.obj || {}; result.obj.time = Date.now(); return result } let data = { title:'loooook!', obj: { name: 'keo', age: '12' } } let res = setTime(data); console.log('res',res); //res { obj: { name: 'keo', age: '12', time: 1533625350183 } } console.log('data',data); //data { title: 'loooook!', obj: { name: 'keo', age: '12', time: 1533625350183 } }
我只是想繼承參數的部分數據,並在此基礎添加一些東西,可是參數 data
的源數據也被我改動了,若是以後有其餘人想要從data
獲取數據,他可能還須要注意是否有像 setTime
這樣的函數調用它。算法
function setTime(data) { let result = {}; result.obj = {}; Object.assign(result.obj,data.obj) result.obj.time = Date.now(); return result }
嗯,或者你也能夠用 for...in
,注意下兩者的不一樣。
咱們知道 Object.assign
只是淺拷貝,若是 data.obj
的屬性值仍然有引用類型的話,那麼仍是會碰見一樣的問題。
那要怎麼辦?難道要遍歷data
下每一個屬性的值?一個個複製過來?咱們看看 lodash
是怎麼作的
你猜的沒錯,的確是要深度遍歷的。
在 baseClone
方法內,拿到要拷貝的對象 value
後,先檢查其類型,而後由對應的 handler 來處理,好比value
是數組類型,則使 result
爲一樣長度的數據,而後對每一項都遞歸調用 baseClone
,直到 value
是非引用類型,返回 value
的值;若是是普通對象類型,則使 result
爲空數組,而後拿取value
的key
,對每一個key
的賦值也是遞歸調用baseClone
。chrome
難道我深拷貝一個變量還要引入 lodash 這麼麻煩嗎 ?沒有簡單點的辦法嗎?api
JSON.parse(JSON.stringify(param))
嗯,可能有點不是那麼酷炫,可是他確實能夠知足要求,並且也無須引入其餘的庫。但若是它真的這麼完美,爲何 lodash 不這麼寫呢?
的確,它的缺點還挺多的,這裏取幾個我以爲比較重要的:數組
{}
是啊,畢竟JSON
的兩個方法自己就只是用來轉換 js 內的對象爲 JSON 格式的,上述幾點甚至都不是缺點,是咱們想借用其餘方法作深拷貝時遇到的問題。瀏覽器
既然是問題那應該能夠解決吧,好比第一條和第二條,在 stringify
時判斷類型,轉化成 帶類型標識符的對象字符串如:Set [1,2,3,4,5]
,而後在parse
的時候對字符串進行解析,特別的類型調用對應的構造函數... 聽起來變得更麻煩了,不要緊,忍忍把各個類型的處理都寫了;針對第三條,拋錯了?不要緊,我 try catch 包起來...,什麼?循環引用?異步
function parse (param){ return JSON.parse(JSON.stringify(param)) } var a = {} var b = {} a['b'] = b b['a'] = a console.log(parse(a)) //TypeError: Converting circular structure to JSON at JSON.stringify
如上代碼, 變量a
和 b
互相引用對方,此時若是借用 JSON 的方法來進行深拷貝的話,會報循環結構轉換轉換 JSON 錯誤。這個問題怎麼解決呢?咱們再翻出 lodash 的源碼看看...函數
// Check for circular references and return its corresponding clone. stack || (stack = new Stack); var stacked = stack.get(value); if (stacked) { return stacked; } stack.set(value, result);
這裏的 value
和 result
分別是是一次遍歷中 要拷貝的值 和 拷貝的結果。stack
是一個用來儲存每次對應的 value
和 result
的對象, stack
下有一塊用於儲存的數組結構,該數組的每一項記錄了單次遍歷中的 value
和 result
,後兩者再次以數組的形式存儲,以 value
作爲下標 0 的項,result
爲下標 1 的項(這裏不用對象的 key-value 形式多是由於循環引用的變量沒法使用 JSON.stringify 轉換成字符串,只能 toString 轉成 object Object);stack
是作爲參數貫穿整個遍歷過程的,每次遍歷時都會以當前的 value
值進行查找(這裏的查找直接是判斷內存地址相等),若是能在 stack
中查到到對應的結果,則直接返回記錄中的result
,再也不繼續遞歸。
好了,循環引用的問題咱們解決了,鼓掌!可是我也放棄使用 JSON 方法了...還有沒有其餘直接點的方法呢?post
結構化克隆算法是由HTML5規範定義的用於複製複雜JavaScript對象的算法,它經過遞歸輸入對象來構建克隆,同時保持先前訪問過的引用的映射,以免無限遍歷循環。性能
怎麼用?
emmm... 它還不能直接使用,你得依靠一些其餘的 API ,間接的使用它。
postMessage()
function StructuredClone(param) { return new Promise(function (res, rej) { const {port1, port2} = new MessageChannel(); port2.onmessage = ev => res(ev.data); port1.postMessage(param); }) } StructuredClone(objects).then(result => console.log(result))
什麼??仍是異步的... 不,我但願能使用同步的方法使用它。
history()
function structuralClone(obj) { const oldState = history.state; history.replaceState(obj, document.title); const copy = history.state; history.replaceState(oldState, document.title); return copy; } const clone = structuralClone(objects);
如你所見,咱們要借用一下 history.replaceState
這個方法,可是咱們不能改變 history
原有的狀態,因此用完就要恢復原狀,當無事發生過。
至少,這是個同步的方法...,若是是同步的場景能夠考慮一下...
這裏的測試代碼是使用的 [Deep-copying in JavaScript] (https://dassur.ma/things/deep... 一文中的,並再次基礎作了一些修改。
單位 μs (繆斯),計算時間的用的接口是 performance.now()
結果精確到5微秒。
...em...Safari瀏覽器在調用完 postMessage 方法後就...沒有而後了...表格都沒刷出來...等了 40 s 終於刷出第一欄...
註釋完 postMessage
又發現不能頻繁的調用 history 。
...em.. 調用 history 相關 api 對 firefox 好像壓力很大,以致於循環都有些錯亂...因而註釋了相關代碼
就結果而言好像看不出什麼區別,多是個人數據很差,你們能夠去看看原文,有展現閱讀性更好的圖表,儘管沒有 lodash 就是了。
回到咱們最初的問題,咱們只是想深拷貝一個 js 對象,若是隻是一個比較"普通"的對象,用JSON的方法簡單又快捷,可是若是這個對象有些「複雜」,彷佛使用 lodash 的方法是比較好的選擇,並且 lodash 連 Structured Clone 算法忽視的 symbol 類型 和 Function 也考慮其中,兼容性也沒問題,也不會在不一樣的瀏覽器發生意外的情況...
lodash 萬歲!lol!!