2010-12-16 122 views
54

如何深入克隆Javascript對象?如何在javascript中進行深度克隆

我知道有基於像JSON.parse(JSON.stringify(o))$.extend(true, {}, o)框架的各種功能,但我不想使用這樣的框架。

什麼是最優雅或有效的方式來創建一個深層克隆。

我們關心像克隆數組這樣的邊緣情況。不打破原型鏈,處理自我參照。

我們不在乎支持DOM對象的複製等,因爲.cloneNode因此存在。

由於我主要想使用深度克隆node.js使用ES5功能的V8引擎是可以接受的。

[編輯]

之前有人建議讓我何況還有從對象prototypically繼承和克隆它創建副本之間的顯着差異。前者弄亂了原型鏈。

[更多編輯]

閱讀你的答案後,我來到了惱人的發現克隆整個物體是非常危險和困難的比賽。舉個例子性關閉對象的下列

var o = (function() { 
    var magic = 42; 

    var magicContainer = function() { 
      this.get = function() { return magic; }; 
      this.set = function(i) { magic = i; }; 
    } 

     return new magicContainer; 
}()); 

var n = clone(o); // how to implement clone to support closures 

有什麼辦法編寫克隆對象,具有在克隆時相同的狀態,但不能改變的o的狀態,而在寫JS解析器的克隆功能JS。

應該沒有現實世界需要這樣的功能了。這僅僅是學術興趣。

+1

它被標記之前重複,我看着http://stackoverflow.com/questions/122102/what-is-其他類型的數據結構中添加功能最有效的方法克隆一個JavaScript對象,並沒有找到任何答案,處理所有的邊緣情況。 – Raynos 2010-12-16 10:52:14

回答

50

這真的取決於你想克隆什麼。這是一個真正的JSON對象還是JavaScript中的任何對象?如果你想做任何克隆,它可能會讓你陷入麻煩。哪個麻煩?我將在下面解釋它,但首先是克隆對象文字,任何原語,數組和節點的代碼示例。

function clone(item) { 
    if (!item) { return item; } // null, undefined values check 

    var types = [ Number, String, Boolean ], 
     result; 

    // normalizing primitives if someone did new String('aaa'), or new Number('444'); 
    types.forEach(function(type) { 
     if (item instanceof type) { 
      result = type(item); 
     } 
    }); 

    if (typeof result == "undefined") { 
     if (Object.prototype.toString.call(item) === "[object Array]") { 
      result = []; 
      item.forEach(function(child, index, array) { 
       result[index] = clone(child); 
      }); 
     } else if (typeof item == "object") { 
      // testing that this is DOM 
      if (item.nodeType && typeof item.cloneNode == "function") { 
       var result = item.cloneNode(true);  
      } else if (!item.prototype) { // check that this is a literal 
       if (item instanceof Date) { 
        result = new Date(item); 
       } else { 
        // it is an object literal 
        result = {}; 
        for (var i in item) { 
         result[i] = clone(item[i]); 
        } 
       } 
      } else { 
       // depending what you would like here, 
       // just keep the reference, or create new object 
       if (false && item.constructor) { 
        // would not advice to do that, reason? Read below 
        result = new item.constructor(); 
       } else { 
        result = item; 
       } 
      } 
     } else { 
      result = item; 
     } 
    } 

    return result; 
} 

var copy = clone({ 
    one : { 
     'one-one' : new String("hello"), 
     'one-two' : [ 
      "one", "two", true, "four" 
     ] 
    }, 
    two : document.createElement("div"), 
    three : [ 
     { 
      name : "three-one", 
      number : new Number("100"), 
      obj : new function() { 
       this.name = "Object test"; 
      } 
     } 
    ] 
}) 

現在,讓我們來談談問題開始真正的克隆對象時,你可能會得到。我現在是在說,對此您做類似

var User = function(){} 
var newuser = new User(); 

當然你也可以克隆它們中創建對象,這不是一個問題,每個對象暴露constructor屬性,你可以用它來克隆對象,但它不會總是工作。你也可以在這個對象上做簡單的for in,但它會走向相同的方向 - 麻煩。我還在代碼中包含了克隆功能,但它被if(false)聲明排除。

那麼,爲什麼克隆可以是一種痛苦?那麼,首先,每個對象/實例都可能有一些狀態。你永遠不能確定你的對象沒有例如一個私有變量,如果是這種情況,通過克隆對象,你只是打破了狀態。

想象一下,沒有狀態,沒關係。那麼我們還有另一個問題。通過「構造函數」方法克隆會給我們另一個障礙。這是一個參數依賴。你永遠可以肯定,那人誰創造了這個對象,沒有做,某種

new User({ 
    bike : someBikeInstance 
}); 

如果是這樣的話,你的運氣了,someBikeInstance很可能在某些環境中創建和上下文未知的克隆方法。

那該怎麼辦?你仍然可以做for in的解決方案,並將這些對象視爲普通的對象文字,但是也許根本不想克隆這些對象,只是傳遞這個對象的引用?

另一個解決方案是 - 您可以設置一個約定,即必須克隆的所有對象都應該自行實現此部分並提供適當的API方法(如cloneObject)。什麼cloneNode正在做的DOM。

你決定。

+0

我來處理使用閉包來隱藏自己狀態的對象的障礙。如何克隆一個對象及其整個狀態,但仍能確保克隆本身不能改變原始狀態。一個couinter指向'result = new item.constructor();'不好的是給定了構造函數&item對象,你應該能夠RE傳遞給構造函數的任何參數。 – Raynos 2010-12-16 12:42:26

+7

@Raynos:如果對象使用閉包來隱藏狀態,那麼你不能克隆它們。因此,術語'關閉'。正如nemisj最後說的,最好的方法是實現一個用於克隆(或序列化/反序列化)的API方法,如果這是一個選項。 – 2010-12-16 13:28:50

+0

@MichielKalkman我有這樣的感覺。雖然有人可能有一個非常聰明的解決方案。 – Raynos 2010-12-16 13:31:23

2

正如其他人已經注意到這個問題和類似的問題一樣,克隆一個「對象」,在一般意義上,在JavaScript中是可疑的。

但是,有一類對象,我將調用「數據」對象,即從{ ... }文字和/或簡單的屬性分配或從JSON反序列化構建的對象,因此需要進行克隆。就在今天,我想人爲地誇大從服務器接收到的數據5 x來測試大數據集會發生什麼,但對象(數組)和它的孩子必須是不同的對象才能正常工作。克隆允許我這樣做是爲了繁衍我的數據集:

return dta.concat(clone(dta),clone(dta),clone(dta),clone(dta)); 

其他地方,我往往最終克隆數據對象是數據提交回,我要在將數據從物體剝離狀態域主機模型發送之前。例如,我可能希望從對象中刪除以「_」開始的所有字段,因爲它已被克隆。

這是我最後寫一般地做到這一點,包括支持陣列和一個選擇器選擇克隆哪些成員(其中使用了「路徑」字符串來確定上下文)的代碼:

function clone(obj,sel) { 
    return (obj ? _clone("",obj,sel) : obj); 
    } 

function _clone(pth,src,sel) { 
    var ret=(src instanceof Array ? [] : {}); 

    for(var key in src) { 
     if(!src.hasOwnProperty(key)) { continue; } 

     var val=src[key], sub; 

     if(sel) { 
      sub+=pth+"/"+key; 
      if(!sel(sub,key,val)) { continue; } 
      } 

     if(val && typeof(val)=='object') { 
      if  (val instanceof Boolean) { val=Boolean(val);  } 
      else if(val instanceof Number) { val=Number (val);  } 
      else if(val instanceof String) { val=String (val);  } 
      else       { val=_clone(sub,val,sel); } 
      } 
     ret[key]=val; 
     } 
    return ret; 
    } 

的最簡單的一種合理的深克隆解決方案,假設非空根對象並沒有成員選擇是:

function clone(src) { 
    var ret=(src instanceof Array ? [] : {}); 
    for(var key in src) { 
     if(!src.hasOwnProperty(key)) { continue; } 
     var val=src[key]; 
     if(val && typeof(val)=='object') { val=_clone(val); } 
     ret[key]=val; 
     } 
    return ret; 
    } 
90

很簡單的方法,也許太簡單了:

var cloned = JSON.parse(JSON.stringify(objectToClone)); 
+4

偉大的,除非對象的值是一個函數,在這一點上,你將不得不使用像接受的答案。或者在Lodash中使用像cloneDeep這樣的輔助函數。 – matthoiland 2015-08-03 05:23:59

+12

如果對象值是一個函數,則該對象不是JSON。 – 2015-09-19 18:30:32

+4

什麼用例可以證明克隆函數而不是僅僅使用它呢? – 2016-03-10 10:19:55

6

Underscore.js contrib library庫有一個叫做從源頭snapshot深克隆對象

片斷功能:

snapshot: function(obj) { 
    if(obj == null || typeof(obj) != 'object') { 
    return obj; 
    } 

    var temp = new obj.constructor(); 

    for(var key in obj) { 
    if (obj.hasOwnProperty(key)) { 
     temp[key] = _.snapshot(obj[key]); 
    } 
    } 

    return temp; 
} 

一旦庫被鏈接到您的項目,調用只需使用

功能
_.snapshot(object); 
+4

好的解決方案,只需要記住一點:克隆和原始的共享原型。如果這是一個問題,它可能會添加「temp .__ proto__ = _.snapshot(obj。__proto__);「正好在」return temp「之上,並且支持可以迭代getOwnPropertyNames()而不是」for(var key in obj)「的屬性標記爲'no enumerate'的內建類」 – 2015-10-21 14:58:30

16

JSON.parse(JSON.stringify())深度複製Javascript對象的組合因爲JSON不支持undefinedfunction() {}的值,因此JSON.stringify會在將JavaScript對象「串化」(編組)爲JSON時忽略這些代碼段。

以下函數將深度複製對象,並且不需要第三方庫(jQuery,LoDash等)。

function copy(aObject) { 
    var bObject, v, k; 
    bObject = Array.isArray(aObject) ? [] : {}; 
    for (k in aObject) { 
    v = aObject[k]; 
    bObject[k] = (typeof v === "object") ? copy(v) : v; 
    } 
    return bObject; 
} 
+0

除了當aObject或它包含的另一個對象)包含自己的自引用... stackoverflow™! – 2017-10-13 23:39:20

+0

@ringø - 您能否提供一些「自引用」測試用例? – tfmontague 2017-10-15 18:10:45

+2

var o = {a:1,b:2}; o [ 「oo」] = {c:3,m:o};' – 2017-10-16 00:54:32

1

我注意到,地圖應該需要特殊的處理,因此在這個線程的所有建議,代碼爲:

function deepClone(obj) { 
    if(!obj || true == obj) //this also handles boolean as true and false 
     return obj; 
    var objType = typeof(obj); 
    if("number" == objType || "string" == objType) // add your immutables here 
     return obj; 
    var result = Array.isArray(obj) ? [] : !obj.constructor ? {} : new obj.constructor(); 
    if(obj instanceof Map) 
     for(var key of obj.keys()) 
      result.set(key, deepClone(obj.get(key))); 
    for(var key in obj) 
     if(obj.hasOwnProperty(key)) 
      result[key] = deepClone(obj[ key ]); 
    return result; 
} 
5

下面是一個ES6的功能,也將與循環引用對象的工作:

function deepClone(obj, hash = new WeakMap()) { 
 
    if (Object(obj) !== obj) return obj; // primitives 
 
    if (hash.has(obj)) return hash.get(obj); // cyclic reference 
 
    const result = obj instanceof Date ? new Date(obj) 
 
       : obj instanceof RegExp ? new RegExp(obj.source, obj.flags) 
 
       : obj.constructor ? new obj.constructor() 
 
       : Object.create(null); 
 
    hash.set(obj, result); 
 
    if (obj instanceof Map) 
 
     Array.from(obj, ([key, val]) => result.set(key, deepClone(val, hash))); 
 
    return Object.assign(result, ...Object.keys(obj).map (
 
     key => ({ [key]: deepClone(obj[key], hash) }))); 
 
} 
 

 
// Sample data 
 
var p = { 
 
    data: 1, 
 
    children: [{ 
 
    data: 2, 
 
    parent: null 
 
    }] 
 
}; 
 
p.children[0].parent = p; 
 

 
var q = deepClone(p); 
 

 
console.log(q.children[0].parent.data); // 1

+0

does not handle date and regexp – mkeremguc 2017-11-09 10:16:36

+0

@mkeremguc,thanks for your comment。我更新了代碼以支持date和regexp。 – trincot 2017-11-09 11:00:18

2

這是深克隆方法我用,我認爲這 大,希望大家提出建議

function deepClone (obj) { 
    var _out = new obj.constructor; 

    var getType = function (n) { 
     return Object.prototype.toString.call(n).slice(8, -1); 
    } 

    for (var _key in obj) { 
     if (obj.hasOwnProperty(_key)) { 
      _out[_key] = getType(obj[_key]) === 'Object' || getType(obj[_key]) === 'Array' ? deepClone(obj[_key]) : obj[_key]; 
     } 
    } 
    return _out; 
} 
0

這適用於數組,對象和原語。雙遞歸算法兩個遍歷方法之間切換:

const deepClone = (objOrArray) => { 

    const copyArray = (arr) => { 
    let arrayResult = []; 
    arr.forEach(el => { 
     arrayResult.push(cloneObjOrArray(el)); 
    }); 
    return arrayResult; 
    } 

    const copyObj = (obj) => { 
    let objResult = {}; 
    for (key in obj) { 
     if (obj.hasOwnProperty(key)) { 
     objResult[key] = cloneObjOrArray(obj[key]); 
     } 
    } 
    return objResult; 
    } 

    const cloneObjOrArray = (el) => { 
    if (Array.isArray(el)) { 
     return copyArray(el); 
    } else if (typeof el === 'object') { 
     return copyObj(el); 
    } else { 
     return el; 
    } 
    } 

    return cloneObjOrArray(objOrArray); 
} 
0

我們可以利用遞歸製作deepcopy的。它可以創建數組,對象,對象數組,對象數組的副本和函數的副本。 如果你願意,你可以像地圖等

function deepClone(obj) { 
     var retObj; 
     _assignProps = function(obj, keyIndex, retObj) { 
       var subType = Object.prototype.toString.call(obj[keyIndex]); 
       if(subType === "[object Object]" || subType === "[object Array]") { 
        retObj[keyIndex] = deepClone(obj[keyIndex]); 
       } 
       else { 
        retObj[keyIndex] = obj[keyIndex]; 
       } 
     }; 

     if(Object.prototype.toString.call(obj) === "[object Object]") { 
      retObj = {}; 
      for(key in obj) { 
       this._assignProps(obj, key, retObj); 
      } 
     } 
     else if(Object.prototype.toString.call(obj) == "[object Array]") { 
      retObj = []; 
      for(var i = 0; i< obj.length; i++) { 
       this._assignProps(obj, i, retObj); 
      } 
     }; 

     return retObj; 
    };