深入探討JavaScript如何實現深度復制(deep clone)


在代碼復用模式里面有一種叫做“復制屬性模式”(copying properties pattern)。談到代碼復用的時候,很有可能想到的是代碼的繼承性(inheritance),但重要的是要記住其最終目標——我們要復用代碼。繼承性只是實現代碼復用的一種手段,而不是唯一的方法。復制屬性也是一種復用模式,它跟繼承性是有所不同的。這種模式中,對象將從另外一個在對象中獲取成員,其方法是僅需將其復制即可。用過jQuery的都知道,它有一個$.extend()方法,它的用途除了擴展第三方插件之外,還可以用來復制屬性的。下面我們來看一個extend()函數的實現代碼(注意這里的並不是jQuery的源碼,只是一個簡單的示例):

function extend(parent, child) {
    var i;

    //如果不傳入第二參數child
    //那么就創建一個新的對象
    child = child || {}; 

    //遍歷parent對象的所有屬性
    //並且過濾原型上的屬性
    //然后將自身屬性復制到child對象上
    for(i in parent) {
        if(parent.hasOwnProperty(i)) {
            child[i] = parent[i];
        }
    }

    //返回目標對象child
    return child;
}

上面的代碼是一個簡單的實現,它僅遍歷父對象的成員並將其復制到子對象中去。下面我們用上面的extend()方法來測試一下:

var dad = {name: "Adam"};
var kid = extend(dad);
console.log(kid.name); //Adam

我們發現,extend()方法已經可以正常工作了。但是有一個問題,上面給出的是一種所謂的淺復制(shallow clone)。在使用淺復制的時候,如果改變了子對象的屬性,並且該屬性恰好又是一個對象,那么這種操作也會修改父對象,單是很多情況這不是我們想要的結果。考慮下列情況:

var dad = {
    counts: [1, 2, 3],
    reads: {paper: true}
};

var kid = extend(dad) //調用extend()方法將dad的屬性復制到kid上面
kid.counts.push(4); //把4追加到kid.counts數組里面
console.log(dad.counts); //[1, 2, 3, 4]

通過上面的例子,我們會發現,修改了kid.counts屬性以后(把元素4追加進去了),dad.counts也會受到影響。這是因為在使用淺復制的時候,由於對象是通過引用傳遞的,即kid.counts和dad.counts指向的是同一個數組(或者說在內存上他們指向同一個堆的地址)

 

下面,讓我們修改extend()函數以實現深度復制。我們需要做的事情就是檢查父對象的每一個屬性,如果該屬性恰好是對象的話,那么就遞歸復制出該對象的屬性。另外,還需要檢測該對象是否為一個數組,這是因為數組的字面量創建方式和對象的字面量創建方式不一樣,前者是[],后者是{}。檢測數組可以使用Object.prototype.toString()方法進行檢測,如果是數組的話,他會返回"[object Array]"。下面我們來看一下深度復制版本的extend()函數:

function extendDeep(parent, child) {
    child = child || {};
    for(var i in parent) {
        if(parent.hasOwnProperty(i)) {
            //檢測當前屬性是否為對象
            if(typeof parent[i] === "object") {
                //如果當前屬性為對象,還要檢測它是否為數組
                //這是因為數組的字面量表示和對象的字面量表示不同
                //前者是[],而后者是{}
                child[i] = (Object.prototype.toString.call(parent[i]) === "[object Array]") ? [] : {};

                //遞歸調用extend
                extendDeep(parent[i], child[i]);
            } else {
                child[i] = parent[i];
            }

        }
    }

    return child;
}

好了,深度復制的函數已經寫好了,下面來測試一下看是否能夠預期那樣子工作,即是否可以實現深度復制:

var dad = {
    counts: [1, 2, 3],
    reads: {paper: true}
};

var kid = extendDeep(dad);

//修改kid的counts屬性和reads屬性
kid.counts.push(4);
kid.reads.paper = false;


console.log(kid.counts); //[1, 2, 3, 4]
console.log(kid.reads.paper); //false

console.log(dad.counts); //[1, 2, 3]
console.log(dad.reads.paper); //true

通過上面例子,我們可以發現,即使修改了子對象的kid.counts和kid.reads,父對象的dad.counts和kid.reads並沒有改變,因此我們的目的實現了。

下面來總結一下實現深復制的的基本思路:

1.檢測當前屬性是否為對象

2.因為數組是特殊的對象,所以,在屬性為對象的前提下還需要檢測它是否為數組。

3.如果是數組,則創建一個[]空數組,否則,創建一個{}空對象,並賦值給子對象的當前屬性。然后,遞歸調用extendDeep函數

上面例子使我們自己使用遞歸算法實現的一種深度復制方法。事實上,ES5新增的JSON對象提供的兩個方法也可以實現深度復制,分別是JSON.stringify()和JSON.parse();前者用來將對象轉成字符串,后者則把字符串轉換成對象。下面我們使用該方法來實現一個深度復制的函數:

function extendDeep(parent, child) {

    var i,
        proxy;

    proxy = JSON.stringify(parent); //把parent對象轉換成字符串
    proxy = JSON.parse(proxy) //把字符串轉換成對象,這是parent的一個副本

    child = child || {};


    for(i in proxy) {
        if(proxy.hasOwnProperty(i)) {
            child[i] = proxy[i];
        }
    }

    proxy = null; //因為proxy是中間對象,可以將它回收掉

    return child;
}

下面是測試例子:

var dad = {
    counts: [1, 2, 3],
    reads: {paper: true}
};

var kid = extendDeep(dad);

//修改kid的counts屬性和reads屬性
kid.counts.push(4);
kid.reads.paper = false;


console.log(kid.counts); //[1, 2, 3, 4]
console.log(kid.reads.paper); //false

console.log(dad.counts); //[1, 2, 3]
console.log(dad.reads.paper); //true

測試發現,它也實現了深度復制。一般推薦使用后面這種方法,因為JSON.parse和JSON.stringify是內置函數,處理起來會比較快。另外,前面的那種方法使用了遞歸調用,我們都知道,遞歸是效率比較低的一種算法。

好了,關於深度復制的就寫到這里了。我也是邊學習邊在這里總結,如果有錯誤之處,懇請提出。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM