深拷貝的幾個誤區
週末賦閒在家,因為太冷了,不想出門,索性宅一天好了。但是閒著沒事做總是很無聊的,正好新的一年想抓一下童鞋同學的程式碼質量,就隨便打開了幾個童鞋寫的程式碼。於是故事就展開了。
團隊大了之後,如何統一團隊程式碼風格其實是一個蠻重要的問題,目前我們團隊使用lint的方式進行了限制,這次的review可以說是初見成效,除了不少同學偷偷摸摸的通過noverify的方式提交程式碼以外。不過沒看多久就發現了一段有趣的程式碼:
function deepClone(obj, res = {}) { const _res = res; for(let key in obj) { if (obj[key] == res) { continue; } if (typeof obj[key] === 'object') { if(Array.isArray(obj[key])){ _res[key] = obj[key].slice(); } else { _res[key] = deepClone(obj[key], _res[key]); } } else { _res[key] = obj[key]; } } return _res; }
初看上去就會好奇,為什麼不使用lodash現成的深拷貝呢?一看是個h5的專案,推測可能是為了整體包的大小做了取捨,也無可厚非吧。不過仔細看程式碼,乍一看好像還挺好,還細心的考慮的陣列的情況,但是再仔細看得時候又覺得好像有什麼地方不對,如果入參是個字串感情你給別人返回一個空物件麼。。跑了一個case發現果然有點問題:
var c = { a:1 }; var d = new Map(); d.set('a', 1); var a = { a: 1, b: true, c: ()=>{console.log(123)}, d: [1,2], e: d, f: c, g: {} } var b = deepClone(a);
雖然正常的處理好像都沒有什麼問題,但是遇到新的資料結構如Map的時候,這種拷貝就會出問題。而且這樣遞迴,層級一深還會有爆棧的隱患,相當的不安全...
所以深拷貝究竟應該怎麼寫呢?
本著能google不手寫的原則,查了下網路,好的寫法沒發現幾個,倒是幾個誤區經有的文章經常會提到且一筆略過:
1、JSON.parse(JSON.stringify(obj)) 的實現究竟算不算深拷貝?
當然算,但是這種實現有幾個潛在風險:
1)它的原理是將能夠JSON化的值JSON化,再重新生成一個新的JSON物件。所以它能夠實現的基礎是這個值是能夠被JSON化的,像諸如function、map、set全是不能JSON化的,一轉就沒了。
2) 它還有一個風險是在處理迴圈引用時是會報錯的。這點很多童鞋在實操的時候特別容易忽略,特別是在node端進行端端通訊的時候,曾經一個報錯查半天,真的是血的教訓。
所以,如果是純JSON的資料的深拷貝且不包含迴圈引用,是可以使用這個方法的
2、遞迴在js中是有風險的
常見的實現都是基於遞迴的,但是遞迴本身在js的runtime,很容易因為層級過深而導致爆棧。
而通常的方式則是通過“拍平”樹級結構的物件成一個數組,來進行拷貝。
3、深拷貝的情況因業務場景的定義會有不同
有些業務場景需要保持拷貝物件中的值的引用關係不變,而有些卻要改變。另外,js的資料結構發展到今天,需要在拷貝時處理的邊界情況已經很多了,你需要好好考慮清楚哪些情況需要怎麼處理。
其實話說回來,仔細看得話,你會發現第一種寫法和jQuery中extend的方式其實是很像的,通常的情況也基本能覆蓋了,不過在現在這個語境下,相比比較“安全”的實現深拷貝,還是建議使用lodash的cloneDeep(相比jQuery和underscore深拷貝大概60行左右的程式碼,lodash使用了近幾百行程式碼,考慮了各種邊界情況,也可謂是業界楷模了)