如何在 javascript 中进行深度克隆

IT技术 javascript
2021-01-13 13:37:03

你如何深度克隆一个 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 解析器就不能改变状态

现实世界应该不再需要这样的功能了。这只是学术兴趣。

6个回答

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

var cloned = JSON.parse(JSON.stringify(objectToClone));
如果我没记错的话,这也会将日期转换为字符串。
2021-03-21 13:37:03
什么用例可以证明克隆一个函数而不是仅仅使用它?
2021-03-23 13:37:03
@G.Ghez 如果克隆一个包含函数的对象,该函数将丢失..
2021-03-24 13:37:03
除非对象值是函数,否则很好,此时您必须使用类似于已接受答案的内容。或者使用类似cloneDeepLodash的辅助函数
2021-03-27 13:37:03
如果对象值是函数,则对象不是 JSON。
2021-03-28 13:37:03

这真的取决于你想克隆什么。这是一个真正的 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 可能是在某个上下文中创建的,而该上下文对于 clone 方法来说是未知的。

那么该怎么办?您仍然可以for in解决问题,并将此类对象视为普通对象文字,但也许根本不克隆此类对象是一个想法,而只是传递此对象的引用?

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

你决定。

@nemisj:标准化 forBoolean将失败,因为new Boolean(new Boolean(false)) => [Boolean: true]. 不是吗?
2021-03-24 13:37:03
@Raynos:如果对象使用闭包来隐藏状态,那么你就不能克隆它们。因此,术语“关闭”。正如 nemisj 最后所说,如果可以选择,最好的方法是实现用于克隆(或序列化/反序列化)的 API 方法。
2021-03-25 13:37:03
我遇到了处理使用闭包来隐藏状态的对象的障碍。如何克隆一个对象及其整个状态,但仍然确保克隆不能自行改变原始状态。一个糟糕的点result = new item.constructor();是,给定构造函数和 item 对象,您应该能够 RE 传递给构造函数的任何参数。
2021-03-27 13:37:03
@GabrielPetrovayif从功能的角度来看,是“无用的”,因为它永远不会运行,但它的学术目的是展示一个人可能尝试使用的假设实现,由于稍后解释的原因,作者不建议这样做。所以,是的,它会else在每次评估条件时触发子句,但代码存在是有原因的。
2021-03-28 13:37:03
@MichielKalkman 我有一种感觉就是这种情况。尽管有人可能对此有一个非常聪明的解决方案。
2021-03-29 13:37:03

JSON.parse(JSON.stringify())深度复制 Javascript 对象组合是一种无效的黑客攻击,因为它适用于 JSON 数据。它不支持undefinedor 的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;
}
@ringø - 你能提供一些“自我参考”的测试用例吗?
2021-03-16 13:37:03
var o = { a:1, b:2 } ; o["oo"] = { c:3, m:o };
2021-03-18 13:37:03
除非 aObject(或它包含的另一个对象)包含对自身的自引用……stackoverflow™!
2021-03-19 13:37:03
此功能简单易懂,几乎可以捕获所有情况。在 JavaScript 世界中,这几乎是您所能获得的完美。
2021-04-02 13:37:03
我喜欢这个解决方案。对我来说唯一的修复是处理空值: bObject[k] = (v === null) ? null : (typeof v === "object") ? copy(v) : v;
2021-04-08 13:37:03

这是一个 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

关于 Sets 和 Maps 的说明

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

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

另一方面,如果 Set/Maphas是一个键,那么这在原始和副本中都应该是正确的——至少在对它们中的任何一个进行任何更改之前都是如此。如果副本是一个 Set/Map,它的键以前从未出现过(因为它们是在克隆过程中创建的),那将会很奇怪:对于需要知道给定对象是否为是否键入该 Set/Map。

正如您所注意到的,我更倾向于第二种意见: Sets 和 Maps 的键是应该保持不变的(可能是引用)。

此类选择通常也会与其他(可能是自定义的)对象一起出现。没有通用的解决方案,因为在很大程度上取决于克隆对象在您的特定情况下的行为方式。

如果我没记错的话,您可能可以通过以下方式添加对 Sets 的支持: if (object instanceof Set) Array.from(object, val => result.add(deepClone(val, hash)));
2021-03-12 13:37:03
不处理日期和正则表达式
2021-03-20 13:37:03
没错,但是如果您正在使用需要不可变数据的反应式代码,那么您也需要具有深度克隆的 Set。我不得不承认,有时我确实想要一个带有指向远程对象的指针的对象,这样我就可以从那里改变它们,而不必接触原始对象。也就是说,随着时间的推移,我似乎越来越不那么聪明了。与其他方式相比,拥有不可变数据在更多情况下更有意义。
2021-03-24 13:37:03
@mkeremguc,感谢您的评论。我更新了代码以支持日期和正则表达式。
2021-03-27 13:37:03
@RobertBiggs,这是一种可能性,但在我看来,如果 Set 具有某个键,那么在该 Set 的克隆版本中也应该如此。使用您建议的代码,如果键是对象,这将不成立。因此,我建议不要克隆密钥——我真的认为它会像预期的那样表现得更好。请参阅我在这方面的答案的更新。
2021-04-01 13:37:03

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);
这不会处理这些情况:(1)道具是符号(2)循环引用
2021-03-20 13:37:03
很好的解决方案,只是要记住一点:克隆和原始共享相同的原型。如果这是一个问题,可以添加“temp.__proto__ = .snapshot(obj.__proto_ );” 就在“return temp”的正上方,为了支持带有标记为“no enumerate”的属性的内置类,您可以迭代 getOwnPropertyNames() 而不是“for (var key in obj)”
2021-03-26 13:37:03