如何在javascript中进行深度克隆

169

如何深度克隆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

有没有一种方法可以编写一个克隆函数,它可以克隆对象,在克隆时具有相同的状态,但是不能在不使用JS解析器的情况下更改o的状态。

现实世界中应该不再需要这样的函数了。这只是纯学术兴趣。


3
在它被标记为重复之前,我查看了https://dev59.com/83VD5IYBdhLWcg3wAGiD,并没有发现任何回答涉及到所有边缘情况。 - Raynos
我今晚一直在阅读关于这个的内容,其中我找到的资源包括一个看起来不太好看的博客文章,其中包括了一些用于访问浏览器中的结构化克隆算法的技巧:https://dassur.ma/things/deep-copy/ - Cat
1
这个回答解决了你的问题吗?在JavaScript中深度克隆对象的最有效方法是什么? - shreyasm-dev
2
当你说“我知道有基于框架的各种函数,比如JSON.parse(JSON.stringify(o))$.extend(true, {}, o),但我不想使用这样的框架”时,我感到困惑。$显然是一个外部库(jQuery),但JSON是JavaScript的一部分。如果你还记得的话,那么对使用JSON有什么顾虑呢?也许是因为在2010年,缺乏IE 6和7的支持 - ruffin
1
@r3wt 公平,但我觉得我的观点是,最终所有这些方法似乎都存在边缘情况漏洞,其中深度克隆过程无法正常工作,并暗示将对象序列化为 JSON 并了解如何重建您的对象模型的规则可能会产生“最佳整体”结果。也就是说,我打赌我们总是可以说,“‘X’策略在需要[m,n,o]的[某些对象子集]上行不通”,并且特别想知道JSON.stringify在OP失败的具体原因。 - ruffin
显示剩余3条评论
26个回答

202

非常简单的方式,也许过于简单了:

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

26
如果对象值是一个函数,那么您将需要使用类似接受的答案那样的东西。或者使用像 Lodash 中的 cloneDeep 这样的辅助函数。总体而言很好,除非对象值是函数。 - matthoiland
45
如果一个对象的值是函数,那么该对象就不是 JSON。 - Jos de Jong
8
在什么情况下克隆一个函数才能被证明比直接使用它更有意义? - G. Ghez
4
如果我没记错的话,这个还可以将日期转换为字符串。 - Peter
5
如果你克隆一个包含函数的对象,那么这个函数将会丢失。 - Peter
显示剩余7条评论

83

这取决于您想克隆什么。这是一个真正的 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";
            }   
        }
    ]
})

现在,让我们谈一谈在开始克隆真实对象时可能会遇到的问题。我现在所说的是指通过类似于下面这样的方式创建的对象:

var User = function(){}
var newuser = new User();
当然可以克隆它们,这不是问题,每个对象都公开了构造函数属性,您可以使用它来克隆对象,但这并不总是起作用。您也可以在这些对象上执行简单的for in操作,但这会带来问题。我还在代码中包含了克隆功能,但它被if(false)语句排除了。
那么,为什么克隆可能是个麻烦?首先,每个对象/实例可能都有一些状态。您永远无法确定您的对象是否具有例如私有变量之类的状态,如果是这种情况,在克隆对象时,您只会破坏状态。
假设没有状态,那就没问题了。那么我们仍然有另一个问题。通过“构造函数”方法进行克隆将给我们带来另一个障碍。它是一个参数依赖性。您永远无法确定创建此对象的人是否做过某种
new User({
   bike : someBikeInstance
});

如果是这种情况,那么你就很遗憾了,someBikeInstance可能是在某个上下文中创建的,而克隆方法不知道该上下文的存在。

那怎么办呢?你仍然可以使用for in的解决方案,并将此类对象视为普通对象文字使用,但最好不要克隆这样的对象,而只需传递此对象的引用即可。

另一个解决方案是 - 您可以设置一个约定,即所有必须克隆的对象都应自行实现此部分并提供适当的API方法(例如cloneObject)。类似DOM中的cloneNode所做的事情。

由您决定。


8
如果对象使用闭包来隐藏状态,那么你就无法克隆它们。这就是“闭包”一词的由来。正如nemisj在结尾所说的那样,最好的方法是实现一个API方法来进行克隆(或序列化/反序列化),如果可能的话。 - Michiel Kalkman
2
@GabrielPetrovay 从功能的角度来看,那个 if 是“无用”的,因为它永远不会运行,但它具有学术目的,展示了一个人们可能尝试使用的假设实现,作者并不建议这样做,因为后面解释的原因。所以,是的,每次评估条件时它都会触发 else 子句,但代码存在的原因是有道理的。 - Gui Imamura
1
@nemisj: 对Boolean进行归一化将会失败,因为new Boolean(new Boolean(false)) => [Boolean: true]。不是这样吗? - sgrtho
对我来说,原始类型的检查不起作用。 例如,"foo" instanceof Stringfalse - Naruyoko
对于布尔值,返回item.valueOf()比返回Boolean(item)更方便,因为Boolean(new Boolean(false))的结果是true - marsgpl
显示剩余4条评论

52

JSON.parse(JSON.stringify())的组合用于深度复制JavaScript对象是一种无效的技巧,因为它是用于JSON数据的。在将Javascript对象转换为JSON时,它不支持undefinedfunction() {}的值,会简单地忽略它们(或将它们设置为null)。

更好的解决方案是使用深拷贝函数。以下函数可以深拷贝对象,并且不需要第三方库(如jQuery、LoDash等)。

function copy(aObject) {
  // Prevent undefined objects
  // if (!aObject) return aObject;

  let bObject = Array.isArray(aObject) ? [] : {};

  let value;
  for (const key in aObject) {

    // Prevent self-references to parent object
    // if (Object.is(aObject[key], aObject)) continue;
    
    value = aObject[key];

    bObject[key] = (typeof value === "object") ? copy(value) : value;
  }

  return bObject;
}

注意: 此代码可以检查简单的自引用(取消注释部分// Prevent self-references to parent object),但在可能的情况下,还应该避免创建具有自引用的对象。请参见:https://softwareengineering.stackexchange.com/questions/11856/whats-wrong-with-circular-references


10
除非 aObject(或其包含的另一个对象)包含对自身的自我引用…… stackoverflow™! - Déjà vu
5
var o = { a:1, b:2 } ; o["oo"] = { c:3, m:o }; - Déjà vu
6
我喜欢这个解决方案。对我来说唯一需要处理的是空值:bObject[k] = (v === null) ? null : (typeof v === "object") ? copy(v) : v; - David Kirkland
2
这个函数简单易懂,几乎可以捕获所有情况,在 JavaScript 的世界中,这已经是完美的了。 - icc97
1
@tfmontague 我认为这正是git和npm的用途,而不是试图在问答网站上迭代代码片段。 - noffle
显示剩余16条评论

45

我们可以使用structuredClone()实现深度复制。

const original = { name: "stack overflow" };


// Clone it
const clone = structuredClone(original);

2
当然可以。目前只支持 Firefox - Suraj Rao
1
同时也支持 Node.js 17+。 - razzkumar
请注意,通过历史记录API,这可以在浏览器中进行填充。 function structuredClone(val) { const { href } = location; const prev = history.state; history.replaceState(val, "", href); const res = history.state; history.replaceState(prev, "", href); return res; } 然而,这个结构化克隆算法将无法处理每种类型的输入,例如函数或DOM元素会抛出异常。 - Kaiido
1
截至本评论时,Can_I_use报告现在可用性达到89.7%。 - 3dGrabber

35

现在 Web API 中有 structuredClone,它也适用于具有循环引用的对象。


之前的回答

以下是一个 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

关于Set和Map的注意事项

如何处理Sets和Maps的键是有争议的:这些键通常是原始类型(在这种情况下没有争议),但它们也可以是对象。此时的问题是:这些键是否应该被克隆?

有人可能会认为应该这样做,以便如果在副本中更改了那些对象,则不会影响原始对象,反之亦然。

另一方面,如果Set/Map has一个键,那么希望在原始版本和副本版本中都是正确的——至少在对它们进行任何更改之前是这样。如果副本是一个Set/Map,它具有以前从未出现过的键(因为它们是在克隆过程中创建的),那将非常奇怪:对于需要知道给定对象是否是该Set/Map中的键的任何代码来说,这显然是没有用的。

正如您注意到的那样,我更倾向于第二种观点:Sets和Maps的键是应该保持不变的(也许是引用)。

这样的选择通常也会在其他(也许是自定义)对象中引起问题。由于克隆对象在特定情况下的预期行为取决于很多因素,因此没有通用的解决方案。


1
不处理日期和正则表达式。 - mkeremguc
1
@mkeremguc,感谢您的评论。我更新了代码以支持日期和正则表达式。 - trincot
很好的解决方案,包含了map。只是缺少对ES6集合克隆的支持。 - Robert Biggs
1
如果我没记错的话,你可以通过以下方式为集合添加支持:if (object instanceof Set) Array.from(object, val => result.add(deepClone(val, hash))); - Robert Biggs
1
@RobertBiggs,这是一种可能性,但我认为如果一个Set有某个特定的键,那么在该Set的克隆版本中也应该是真实的。如果使用您建议的代码,则如果键是对象,则不会成立。因此,我建议不要克隆键 - 我真的认为它会更符合预期。请参见我的答案更新。 - trincot
1
如果你正在处理期望不可变数据的响应式代码,那么你需要深度克隆的集合。我必须承认,有时我想要一个带有指向远程对象的指针的对象,这样我就可以从那里进行突变,而不是必须到达原始对象。话虽如此,随着时间的推移,我似乎越来越少这样做了。在更多情况下,使用不可变数据比另一种方式更有意义。 - Robert Biggs

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

一旦将库链接到您的项目中,只需使用以下方式调用函数即可:

invoke the function simply using

_.snapshot(object);

5
好的解决方案,只需记住一点:克隆和原始对象共享同一个原型。如果这是一个问题,可以在 "return temp" 上方添加 "temp.proto = _.snapshot(obj.proto);" 来解决,另外,为了支持具有标记为'no enumerate'的属性的内置类,可以使用 getOwnPropertyNames() 进行迭代,而不是使用 "for (var key in obj)"。 - Ronen Ness
1
这将无法处理以下情况:(1)props 是符号(2)循环引用。 - Danyu

4

正如其他人在本问题和类似问题上指出的那样,在JavaScript中,克隆“对象”在一般意义上是可疑的。

然而,有一类对象,我称之为“数据”对象,即仅从{ ... }文字和/或简单属性赋值构造或从JSON反序列化的对象,需要对其进行克隆是合理的。就在今天,我想将从服务器接收到的数据人为地增加5倍,以测试大型数据集发生的情况,但对象(一个数组)及其子对象必须是不同的对象才能正常运行。克隆让我能够这样做以扩大我的数据集:

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

我经常需要克隆数据对象以便将数据提交回主机,并在发送之前从数据模型中去除对象中的状态字段。例如,我可能想要从克隆的对象中剥离所有以"_"开头的字段。

以下是我最终编写的通用代码,包括支持数组和选择器来选择要克隆的成员(它使用"path"字符串来确定上下文):

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

4

1
问题说:“我不想使用库”。 - Femi Oni
@FemiOni 这个问题并没有涉及到任何库(即使在旧版本中也是如此)...这里还有一些其他答案也使用了一个或多个库。 - CPHPython
1
@FemiOni昨天的回答被踩了。我想知道...无论如何,这是一个学习的地方,以防有人真的要实现深度克隆本身,lodash源代码baseClone可能会提供一些思路。 - CPHPython
@FemiOni,JSON 对象既不是库也不是框架... 如果你要实现这个函数,我建议你查看其中一个开源库并使用你需要的部分(许多已经经过多年测试)。这将避免长期存在的错误和遗漏的考虑。 - CPHPython

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

对象的深度克隆可以通过多种方式实现,但每种方式都有其自身的限制,如下所述。因此,我建议您使用structuredClone算法。

  • JSON.parse(JSON.stringify(object)) - 不会复制函数、日期、未定义等内容。

const obj = {
  name: 'alpha',
  printName: function() {
    console.log(this.name);
  }
};

console.log(JSON.parse(JSON.stringify(obj))); // function not copied

  • _.cloneDeep(object) - 这是一个不错的选择,但需要使用lodash库。

const obj = {
  name: 'alpha',
  printName: function() {
    console.log(this.name);
  }
};

filteredArray = _.cloneDeep(obj);
console.log(filteredArray)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/1.2.1/lodash.min.js"></script>

  • structuredClone(object) - 浏览器原生API(它比使用JSON.parse()和JSON.stringify()更好,因为不会序列化循环对象、Map、Set、Date、RegEx等)

const a = { x: 20, date: new Date() };
a.c = a;

console.log(structuredClone(a)); // { x: 20, date: <date object>, c: <circular ref> }

console.log(JSON.parse(JSON.stringify(a))); // throwing a TypeError


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接