1. 程式人生 > >深拷貝的幾個誤區

深拷貝的幾個誤區

  週末賦閒在家,因為太冷了,不想出門,索性宅一天好了。但是閒著沒事做總是很無聊的,正好新的一年想抓一下童鞋同學的程式碼質量,就隨便打開了幾個童鞋寫的程式碼。於是故事就展開了。

  團隊大了之後,如何統一團隊程式碼風格其實是一個蠻重要的問題,目前我們團隊使用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使用了近幾百行程式碼,考慮了各種邊界情況,也可謂是業界楷模了)