如何在javascript中进行深度克隆


105

您如何深度克隆Javascript对象?

我知道有各种基于类似的框架的功能JSON.parse(JSON.stringify(o))$.extend(true, {}, o)但我不想使用类似的框架。

创建深度克隆的最优雅或最有效的方法是什么。

我们确实关心诸如克隆数组之类的边缘情况。不打破原型链,处理自我参考。

我们不在乎是否支持DOM对象的复制,因为.cloneNode存在该原因。

由于我主要想在node.js使用V8引擎的ES5功能时使用深度克隆,因此可以接受。

[编辑]

在有人建议我之前,先通过原型继承对象创建副本与克隆副本之间存在明显的区别。前者使原型链一团糟。

[进一步编辑]

阅读完您的答案后,我发现了一个令人讨厌的发现,即克隆整个对象是一个非常危险且困难的游戏。以以下基于闭包的对象为例

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

有没有办法编写一个克隆对象的克隆函数,在克隆时具有相同的状态,但是如果不用oJS编写JS解析器就无法改变其状态。

现实世界中不再需要这种功能。这仅仅是学术上的兴趣。


2
在它被标记为重复之前,我查看了stackoverflow.com/questions/122102/…,但是没有找到任何能解决所有极端情况的答案。
雷诺斯

如果没有对象本身的“帮助”,那么“进一步编辑”部分中的要求是无法实现的,因为此类私有变量是真正的私有变量,因此无法通过泛型clone函数访问。有问题的对象应公开自己的量身定制的clone方法。
特里科特

今晚我一直在阅读有关此内容的文章,并且在发现的资源中有一篇看起来很丑陋的博客文章,其中包括一些用于在浏览器中访问结构化克隆算法的技巧

Answers:


67

这实际上取决于您要克隆的内容。这是真正的JSON对象还是JavaScript中的任何对象?如果您想进行任何克隆,可能会给您带来麻烦。哪个麻烦 我将在下面解释它,但是首先,是一个代码示例,该示例克隆对象文字,任何基元,数组和DOM节点。

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") {
                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";
            }   
        }
    ]
})

现在,让我们讨论开始克隆REAL对象时可能遇到的问题。我现在说的是关于您通过执行类似操作创建的对象

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

当然,您可以克隆它们,这不是问题,每个对象都公开构造函数属性,并且可以使用它来克隆对象,但是它并不总是有效。您也可以for in对这个对象做简单的事情,但是它朝着相同的方向发展-麻烦。我还在代码中包含了克隆功能,但if( false )语句未将其包含在内。

那么,为什么克隆会很痛苦呢?好吧,首先,每个对象/实例都可能具有某种状态。您永远无法确定您的对象没有私有变量,如果是这种情况,通过克隆对象,您就可以破坏状态。

想象没有状态,那很好。然后我们还有另一个问题。通过“构造函数”方法进行克隆将给我们带来另一个障碍。这是一个参数依赖项。您永远无法确定创建此对象的人没有这样做,

new User({
   bike : someBikeInstance
});

在这种情况下,您很不走运,可能在某些上下文中创建了someBikeInstance,并且该上下文对于克隆方法而言是未知的。

那么该怎么办?您仍然可以执行for in解决方案,并将此类对象像普通的对象文字一样对待,但是也许不完全克隆此类对象,而只是传递该对象的引用是一种主意吗?

另一个解决方案是-您可以设置一个约定,必须克隆的所有对象都应自己实现这一部分,并提供适当的API方法(例如cloneObject)。cloneNode正在为DOM 做些什么。

你决定。


我遇到了处理使用闭包自己隐藏状态的对象的障碍。如何克隆一个对象及其整个状态,但仍要确保克隆本身不能更改原始状态。一个result = new item.constructor();不好的提示是,给定构造函数和item对象,您应该能够重新传递传递给构造函数的任何参数。
雷诺斯(Raynos)2010年

7
@Raynos:如果对象使用闭包来隐藏状态,则无法克隆它们。因此,术语“关闭”。正如nemisj最后说的,最好的方法是实现一个用于克隆(或序列化/反序列化)的API方法(如果可以的话)。
Michiel Kalkman 2010年

@MichielKalkman我有一种感觉。尽管有人可能对此有一个非常聪明的解决方案。
雷诺斯(Raynos)2010年

有什么作用false && item.constructor?那不是if没用吗?
加百利·彼得罗维

2
@GabrielPetrovay if从功能的角度来看这是“无用的”,因为它永远不会运行,但是它的学术目的是显示一个可能尝试使用的假设实现,由于稍后解释的原因,作者不建议这样做。因此,是的,它将else在条件被评估的所有条件下触发子句,但是仍有理由将代码存在。
2015年

155

很简单的方法,也许太简单了:

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

12
除非对象值是函数,否则很好,这时您必须使用已接受的答案之类的东西。或使用cloneDeepLodash中的辅助功能。
matthoiland

31
如果对象值是一个函数,则该对象不是JSON。
Jos de Jong 2015年

5
哪种用例可以证明克隆功能而不是仅仅使用它?
G. Ghez

3
如果我没记错的话,这也会将日期转换为字符串。
彼得

3
@ G.Ghez假如克隆了一个包含一个函数,函数就会丢失一个对象..
彼得·

38

JSON.parse(JSON.stringify())深度复制Javascript对象的组合是无效的hack,因为它是用于JSON数据的。它不支持undefined或的值function () {},并且在将nullJavascript对象“字符串化”(编组)为JSON时将简单地忽略它们。

更好的解决方案是使用深度复制功能。下面的函数深层复制对象,不需要第三者库(jQuery,LoDash等)。

function copy(aObject) {
  if (!aObject) {
    return aObject;
  }

  let v;
  let bObject = Array.isArray(aObject) ? [] : {};
  for (const k in aObject) {
    v = aObject[k];
    bObject[k] = (typeof v === "object") ? copy(v) : v;
  }

  return bObject;
}

7
除了aObject(或它包含的另一个对象)何时包含对自身的自引用... stackoverflow™!
e2-e4

@ringø-您可以提供一些“自参考”测试用例吗?
tfmontague '17

4
var o = { a:1, b:2 } ; o["oo"] = { c:3, m:o };
e2-e4

4
我喜欢这个解决方案。对我来说唯一的解决方法是处理空值: bObject[k] = (v === null) ? null : (typeof v === "object") ? copy(v) : v;
David Kirkland

2
此功能简单易懂,几乎可以处理所有情况。在JavaScript世界中,您将获得尽可能完美的体验。
icc97 '18

21

这是一个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 Set ? new Set(obj) // See note about this!
                 : obj instanceof Map ? new Map(Array.from(obj, ([key, val]) => 
                                        [key, deepClone(val, hash)])) 
                 : obj instanceof Date ? new Date(obj)
                 : obj instanceof RegExp ? new RegExp(obj.source, obj.flags)
                 // ... add here any specific treatment for other classes ...
                 // and finally a catch-all:
                 : obj.constructor ? new obj.constructor() 
                 : Object.create(null);
    hash.set(obj, result);
    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

关于布景和地图的注释

如何处理设置和地图的钥匙是值得商榷的:这些键通常是原语(在这种情况下,没有辩论),但他们可以也是对象。在这种情况下,问题就变成了:应该克隆那些密钥吗?

可能有人争辩说应该这样做,以便如果副本中的那些对象发生突变,则原始对象不会受到影响,反之亦然。

另一方面,如果设置has键/映射键,则在原件和副本中都应如此-至少在对它们中的任何一个进行更改之前都应如此。如果副本将是一个Set / Map,而该Set / Map具有以前从未发生过的键(因为它们是在克隆过程中创建的),那将很奇怪:对于需要知道给定对象是否为某个键的任何代码,这肯定不是很有用。是否键入该Set / Map。

如您所见,我更多是第二意见:Sets和Maps的键是应该保持不变的(也许是reference)。

这样的选择通常还会与其他(也许是自定义的)对象一起出现。没有通用的解决方案,很大程度上取决于在特定情况下预期克隆对象的行为。


不处理日期和正则表达式
mkeremguc

1
@mkeremguc,感谢您的评论。我更新了代码以支持日期和正则表达式。
Trincot

不错的解决方案,包括地图。只是缺少对克隆es6集的支持。
罗伯特·比格斯

如果我没记错的话,您可以通过以下方式添加对Sets的支持:if (object instanceof Set) Array.from(object, val => result.add(deepClone(val, hash)));
Robert Biggs

@RobertBiggs,这是一种可能性,但我认为如果Set具有某个键,则在该Set的克隆版本中也应该如此。对于您建议的代码,如果键是对象,则将不成立。因此,我建议不要克隆密钥-我确实认为它会表现出更多的预期。请参阅这方面的最新答案。
特里科特

10

Underscore.js的contrib库库有一个称为函数快照深克隆对象

摘录来源:

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_);” 就在“返回温度”的正上方,并且为了支持带有标记为“无枚举”的属性的内置类,您可以在getOwnPropertyNames()上进行迭代,而不是“ for(obj中的var key)”
Ronen Ness

3

这是我使用的深度克隆方法,我认为很好,希望您提出建议

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;
}

3

正如其他人在这个问题和类似问题上所指出的那样,在JavaScript中克隆“对象”通常是可疑的。

但是,有一类对象,我称其为“数据”对象,即仅从{ ... }文字和/或简单的属性分配构造的对象,或从JSON反序列化的对象,因此有必要对其进行克隆。就在今天,我想人为地增加从服务器接收的数据的5倍,以测试大型数据集会发生什么,但是对象(数组)及其子对象必须是不同的对象,事物才能正常运行。克隆使我可以执行以下操作来增加数据集:

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;
    }

2

罗短跑,现在的超Underscore.js,有几个深克隆功能:

根据作者本人的回答

lodash underscore 提供内部版本以确保与最新稳定版本的Underscore兼容。


问题说“我不想使用图书馆”
Femi Oni

@FemiOni这个问题与库没有任何关系(即使是在其旧版本中也是如此)...这里的其他一些答案也正在使用一个或另一个库。
CPHPython

昨天@FemiOni的答案被否决了。我想知道...无论如何,这是一个学习的地方,以防万一有人真的要实现深度克隆本身,lodash源代码baseClone可能会提供一些想法。
CPHPython

@FemiOni JSON对象既不是库也不是框架...如果要实现该功能,建议您研究一个开源库并使用所需的部件(许多部件已经过多年测试)。从长远来看,它将避免错误和遗漏的注意事项。
CPHPython

2

以下功能是深度克隆javascript对象的最有效方法。

function deepCopy(obj){
    if (!obj || typeof obj !== "object") return obj;

    var retObj = {};

    for (var attr in obj){
        var type = obj[attr];

        switch(true){
            case (type instanceof Date):
                var _d = new Date();
                _d.setDate(type.getDate())
                retObj[attr]= _d;
                break;

            case (type instanceof Function):
                retObj[attr]= obj[attr];
                break;

            case (type instanceof Array):
                var _a =[];
                for (var e of type){
                    //_a.push(e);
                    _a.push(deepCopy(e));
                }
                retObj[attr]= _a;
                break;

            case (type instanceof Object):
                var _o ={};
                for (var e in type){
                    //_o[e] = type[e];
                    _o[e] = deepCopy(type[e]);
                }
                retObj[attr]= _o;
                break;

            default:
                retObj[attr]= obj[attr];
        }
    }
    return retObj;
}

var obj = {
    string: 'test',
    array: ['1'],
    date: new Date(),
    object:{c: 2, d:{e: 3}},
    function: function(){
        return this.date;
    }
};

var copyObj = deepCopy(obj);

console.log('object comparison', copyObj === obj); //false
console.log('string check', copyObj.string === obj.string); //true
console.log('array check', copyObj.array === obj.array); //false
console.log('date check', copyObj2.date === obj.date); //false
console.log('object check', copyObj.object === obj.object); //false
console.log('function check', copyObj.function() === obj.function()); //true

2
您有任何论据支持您的主张吗?
Fabian von Ellerts

我在函数下面给出了一个例子。你有什么疑问吗?
Kooldandy

这些示例表明该功能正在运行,这很酷,但是为什么它是“最有效的方式”呢?
Fabian von Ellerts

因为它是在单循环中递归地复制对象的属性。还有日期,函数,对象,数组,数字,字符串,所有这些都可以正确处理。您还有其他办法吗?
Kooldandy

2

现实世界中不再需要这种功能。这仅仅是学术上的兴趣。

纯粹是一种练习,这是一种更实用的方法。这是@tfmontague答案的扩展,因为我建议在此处添加一个防护块。但是,当我感到自己不得不使用ES6并使所有功能实现功能化时,这是我的原型。由于使您必须在数组上进行映射并在对象上进行归约,因此使逻辑变得复杂,但是避免了任何突变。

const cloner = (x) => {
    const recurseObj = x => (typeof x === 'object') ? cloner(x) : x
    const cloneObj = (y, k) => {
        y[k] = recurseObj(x[k])
        return y
    }
    // Guard blocks
    // Add extra for Date / RegExp if you want
    if (!x) {
        return x
    }
    if (Array.isArray(x)) {
        return x.map(recurseObj)
    }
    return Object.keys(x).reduce(cloneObj, {})
}
const tests = [
    null,
    [],
    {},
    [1,2,3],
    [1,2,3, null],
    [1,2,3, null, {}],
    [new Date('2001-01-01')], // FAIL doesn't work with Date
    {x:'', y: {yx: 'zz', yy: null}, z: [1,2,3,null]},
    {
        obj : new function() {
            this.name = "Object test";
        }
    } // FAIL doesn't handle functions
]
tests.map((x,i) => console.log(i, cloner(x)))


1

我注意到Map应该需要特殊对待,因此在该线程中的所有建议下,代码将是:

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;
}

1

我对所有答案的补充

deepCopy = arr => {
  if (typeof arr !== 'object') return arr
  if(arr.pop) return [...arr].map(deepCopy)
  const copy = {}
  for (let prop in arr)
    copy[prop] = deepCopy(arr[prop])
  return copy
}

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;
    };

0

使用immutableJS

import { fromJS } from 'immutable';

// An object we want to clone
let objA = { 
   a: { deep: 'value1', moreDeep: {key: 'value2'} } 
};

let immB = fromJS(objA); // Create immutable Map
let objB = immB.toJS(); // Convert to plain JS object

console.log(objA); // Object { a: { deep: 'value1', moreDeep: {key: 'value2'} } }
console.log(objB); // Object { a: { deep: 'value1', moreDeep: {key: 'value2'} } }

// objA and objB are equalent, but now they and their inner objects are undependent
console.log(objA === objB); // false
console.log(objA.a === objB.a); // false
console.log(objA.moreDeep === objB.moreDeep); // false

lodash /合并

import merge from 'lodash/merge'

var objA = {
    a: [{ 'b': 2 }, { 'd': 4 }]
};
// New deeply cloned object:
merge({}, objA ); 

// We can also create new object from several objects by deep merge:
var objB = {
    a: [{ 'c': 3 }, { 'e': 5 }]
};
merge({}, objA , objB ); // Object { a: [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }

0

这个使用循环引用对我有用

 //a test-object with circular reference :
 var n1 = {   id:0,   text:"aaaaa",   parent:undefined} 
 var n2 = {  id:1,   text:"zzzzz",   parent:undefined } 
 var o = { arr:[n1,n2],   parent:undefined } 
 n1.parent = n2.parent = o;
 var obj = {   a:1,   b:2,   o:o }
 o.parent = obj;

 function deepClone(o,output){ 

     if(!output) output = {};  
     if(o.______clone) return o.______clone;
     o.______clone = output.______clone = output;

   for(var z in o){

     var obj = o[z];
     if(typeof(obj) == "object") output[z] = deepClone(obj)
     else output[z] = obj; 
    }

   return output;
}

console.log(deepClone(obj));

0

var newDate =新日期(this.oldDate); 我正在将oldDate传递给函数并从this.oldDate生成newDate,但是它也在更改this.oldDate。所以我使用了该解决方案,它起作用了。


0

使用[... target]或{... target}时,此解决方案将避免递归问题

function shallowClone(target) {
  if (typeof a == 'array') return [...target]
  if (typeof a == 'object') return {...target}
  return target
}

/* set skipRecursion to avoid throwing an exception on recursive references */
/* no need to specify refs, or path -- they are used interally */
function deepClone(target, skipRecursion, refs, path) {
  if (!refs) refs = []
  if (!path) path = ''
  if (refs.indexOf(target) > -1) {
    if (skipRecursion) return null
    throw('Recursive reference at ' + path)
  }
  refs.push(target)
  let clone = shallowCopy(target)
  for (i in target) target[i] = deepClone(target, refs, path + '.' + i)
  return clone
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.