如何在javascript中深度克隆

Ray*_*nos 95 javascript

你如何深度克隆Javascript对象?

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

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

我们关心像克隆数组这样的边缘情况.不打破原型链,处理自我引用.

我们不关心支持复制DOM对象,因为因为.cloneNode这个原因而存在.

由于我主要想使用深度克隆来node.js使用V5引擎的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
Run Code Online (Sandbox Code Playgroud)

有没有办法编写克隆对象的克隆函数,在克隆时具有相同的状态,但是如果不在oJS中编写JS解析器,则无法改变状态.

这样的功能不再需要现实世界.这仅仅是学术兴趣.

G. *_*hez 138

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

var cloned = JSON.parse(JSON.stringify(objectToClone));
Run Code Online (Sandbox Code Playgroud)

  • 如果对象值是函数,则该对象不是JSON. (28认同)
  • 伟大的除非对象值是一个函数,此时你将不得不使用像接受的答案.或者在Lodash中使用类似`cloneDeep`的辅助函数. (10认同)
  • 什么用例可以证明克隆函数而不仅仅是使用它? (5认同)
  • 如果我没记错的话,这也会将日期转换为字符串。 (3认同)
  • @ G.Ghez如果克隆一个包含函数的对象,该函数将丢失.. (3认同)
  • OP声明*“我知道有各种函数,例如JSON.parse(JSON.stringify(o))...……但是**我不想使用这样的框架。**” * 。因此,这是a)重复问题中的内容,并且b)不是正确的答案。 (2认同)
  • 如果对象包含循环引用,则这种类型的复制会导致错误; (2认同)

nem*_*isj 63

这真的取决于你想要克隆的东西.这是一个真正的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";
            }   
        }
    ]
})
Run Code Online (Sandbox Code Playgroud)

现在,让我们来谈谈开始克隆REAL对象时可能遇到的问题.我现在正在谈论关于通过做类似事情创建的对象

var User = function(){}
var newuser = new User();
Run Code Online (Sandbox Code Playgroud)

当然你可以克隆它们,这不是问题,每个对象都暴露构造函数属性,你可以使用它来克隆对象,但它并不总是有效.你也可以for in对这个对象做简单的事情,但它会走向同一个方向 - 麻烦.我还在代码中包含了克隆功能,但它被if( false )语句排除在外.

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

想象一下,没有州,那没关系.然后我们还有另一个问题.通过"构造函数"方法克隆将给我们带来另一个障碍.这是一个参数依赖.你永远无法确定,创造这个对象的人,没有做过某种形式

new User({
   bike : someBikeInstance
});
Run Code Online (Sandbox Code Playgroud)

如果是这种情况,你运气不好,someBikeInstance可能是在某些上下文中创建的,而且上下文对于clone方法来说是未知的.

那么该怎么办?你仍然可以做for in解决方案,并将这些对象视为普通对象文字,但也许根本不克隆这些对象,只是传递这个对象的引用?

另一种解决方案是 - 您可以设置一个约定,即必须克隆的所有对象应该自己实现此部分并提供适当的API方法(如cloneObject).什么事情cloneNode是做对DOM.

你决定.

  • @Raynos:如果对象使用闭包来隐藏状态,那么就无法克隆它们.因此,术语"关闭".正如nemisj最后所说,最好的方法是实现克隆(或序列化/反序列化)的API方法,如果这是一个选项. (7认同)
  • @GabrielPetrovay从功能的角度来看,"if"是"无用的",因为它永远不会运行,但它的学术目的是展示一个人可能会尝试使用的假设实现,由于解释的原因,作者没有建议后来.所以,是的,它会在每次评估条件时触发`else`子句,但是有代码存在的原因. (2认同)

小智 49

我们可以通过使用StructuredClone()来实现深度克隆

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


// Clone it
const clone = structuredClone(original);
Run Code Online (Sandbox Code Playgroud)

  • 当然..目前仅受[firefox]支持(https://developer.mozilla.org/en-US/docs/Web/API/structedClone#browser_compatibility) (2认同)

tfm*_*gue 34

JSON.parse(JSON.stringify())深度复制Javascript对象的组合是一种无效的黑客攻击,因为JSON不支持undefined和的值function () {},因此在null将Javascript对象"字符串化"(编组)到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;
}
Run Code Online (Sandbox Code Playgroud)

  • 除非aObject(或它包含的其他对象)包含对自身的自引用... stackoverflow™! (7认同)
  • `var o = {a:1,b:2}; o ["oo"] = {c:3,m:o};` (4认同)
  • 我喜欢这个解决方案.只有我解决的问题是处理空值:`bObject [k] =(v === null)?null :( typeof v ==="object")?copy(v):v;` (3认同)
  • 此功能简单易懂,几乎可以捕获所有情况。在 JavaScript 世界中,这几乎是您所能获得的完美。 (2认同)

tri*_*cot 13

这是一个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
Run Code Online (Sandbox Code Playgroud)

关于集合和地图的注释

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

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

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

正如您所注意到的,我更多的是第二种观点:集合和映射的键是(可能是引用)应该保持不变.

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


sva*_*rog 11

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;
}
Run Code Online (Sandbox Code Playgroud)

将库链接到项目后,只需使用即可调用该函数

_.snapshot(object);
Run Code Online (Sandbox Code Playgroud)

  • 好的解决方案,只需记住一点:克隆和原始共享相同的原型.如果这是一个问题,可以添加"temp .__ proto__ = _.snapshot(obj .__ proto__);" 在"返回temp"之上,并且为了支持标记为"no enumerate"的属性的内置类,您可以迭代getOwnPropertyNames()而不是"for(obj中的var key)" (4认同)