克隆:除了JSON.parse(JSON.stringify(x)),还有什么更快的替代方法吗?

38
什么是最快的替代方法?
JSON.parse(JSON.stringify(x))

有一种更好/内置的方法可以对对象/数组执行深度克隆,但我还没有找到它。

有什么想法吗?


3
这个问题有些令人困惑 - 你是在谈论一般的JavaScript对象(数组并不属于JavaScript对象),还是JSON这种JavaScript对象的子集? - Anders
JSON序列化和解析非常缓慢,因此创建对象的任何本地方法可能会更快。另外需要注意的是,DOM对象具有本地的clone()方法。 - jishi
3
可能是重复的问题:什么是克隆JavaScript对象的最有效方法? - jishi
@jishi 你的意思是 Node 有一个 .cloneNode 方法? - Raynos
@Raynos:啊,是的,我的错。用了太多jQuery。 - jishi
JSON.parse(JSON.stringify(x)) 不是克隆对象的有效方法。如果您有一个作为对象成员的方法,该方法将不会被复制。只需尝试运行此代码 JSON.parse(JSON.stringify({ a: '1', foo: (a) => a*a }))。您将看到 foo 方法已被删除。 - Gil Epshtain
4个回答

18
没有内置的方法可以深度克隆对象。
深度克隆是一件棘手和边缘化的事情。
假设一个方法deepClone(a)应该返回b的"深度克隆"。
现在,"深度克隆"是一个具有相同[[Prototype]]并拥有所有自己属性克隆的对象。
对于每个被克隆的克隆属性,如果它有自己可以被克隆的属性,则进行递归克隆。
当然,我们保持像[[Writable]]和[[Enumerable]]这样的属性元数据完整。如果不是对象,我们将直接返回该值。
var deepClone = function (obj) {
    try {
        var names = Object.getOwnPropertyNames(obj);
    } catch (e) {
        if (e.message.indexOf("not an object") > -1) {
            // is not object
            return obj;
        }    
    }
    var proto = Object.getPrototypeOf(obj);
    var clone = Object.create(proto);
    names.forEach(function (name) {
        var pd = Object.getOwnPropertyDescriptor(obj, name);
        if (pd.value) {
            pd.value = deepClone(pd.value);
        }
        Object.defineProperty(clone, name, pd);
    });
    return clone;
};

对于许多边缘情况,这种方法会失败。

实例演示

如您所见,通常无法深度克隆对象而不破坏其特殊属性(例如数组中的.length)。要修复此问题,您必须单独处理Array,然后再单独处理每个特殊对象。

当您执行deepClone(document.getElementById("foobar"))时,您期望会发生什么?

此外,浅拷贝很容易。

Object.getOwnPropertyDescriptors = function (obj) {
    var ret = {};
    Object.getOwnPropertyNames(obj).forEach(function (name) {
        ret[name] = Object.getOwnPropertyDescriptor(obj, name);
    });
    return ret;
};

var shallowClone = function (obj) {
    return Object.create(
        Object.getPrototypeOf(obj),
        Object.getOwnPropertyDescriptors(obj)
    );
};

1
边缘案例包括循环引用和宿主对象引用? - katspaugh
1
@katspaugh 和像 Array 及其 .length 魔法一样的特殊对象,我认为 Date 也有其中的魔法。还有宿主对象,是的。我不知道循环引用的简单解决方法是什么。 - Raynos
@Raynos:Date 没有任何魔力。我们已经讨论过了 :-p - Andy E
3
@AndyE 你为什么说谎。日期具有魔力 console.log(Object.create(Date.prototype)); console.log(new Date()); - Raynos
@Raynos:这就像说String有魔力一样 - console.log((new String()).hasOwnProperty("length"), Object.create(String.prototype).hasOwnProperty("length")) - “魔力”显然是在构造函数中完成的。 - Andy E

10

6

我实际上是在比较它和angular.copy

您可以在此处运行JSperf测试: https://jsperf.com/angular-copy-vs-json-parse-string

我在比较:

myCopy = angular.copy(MyObject);

vs

myCopy = JSON.parse(JSON.stringify(MyObject));

这是我在所有电脑上运行的最快速的测试。 这里输入图片描述


13
因为你们两个都叫Alex,我很困惑,以为你在和自己说话。 - George
JSON解析将删除未定义或匿名空函数var orig = {a: "A", b: undefined}; var assigned = Object.assign({}, orig); //{a: "A", b: undefined} var jsoned = JSON.parse(JSON.stringify(orig)) // {a: "A"}https://medium.com/@pmzubar/why-json-parse-json-stringify-is-a-bad-practice-to-clone-an-object-in-javascript-b28ac5e36521 - Mahesh

0

循环引用并不是真正的问题。我的意思是,它们确实是问题,但这只是一个适当记录的问题。无论如何,对于这个问题的快速答案,请查看:

https://github.com/greatfoundry/json-fu

在我疯狂的 JavaScript 实验室中,我一直在使用基本实现来序列化整个 Chromium 的 JavaScript 上下文,包括整个 DOM,并通过 WebSocket 将其发送到 Node 并成功地重新序列化。唯一有问题的循环问题是 navigator.mimeTypes 和 navigator.plugins 无限循环引用,但这很容易解决。
(function(mimeTypes, plugins){
    delete navigator.mimeTypes;
    delete navigator.plugins;
    var theENTIREwindowANDdom = jsonfu.serialize(window);
    WebsocketForStealingEverything.send(theENTIREwindowANDdom);
    navigator.mimeTypes = mimeTypes;
    navigator.plugins = plugins;
})(navigator.mimeTypes, navigator.plugins);

JSONFu使用创建代表更复杂数据类型的Sigils的策略。例如,MoreSigil表示该项已缩写,并且可以请求X个更深层次。重要的是要理解,如果您正在序列化EVERYTHING,则将其恢复到原始状态显然更加复杂。我一直在尝试各种事物,看看可能性,合理性以及最终的理想状态。对我来说,目标比大多数需求更为宏伟,因为我试图尽可能接近将两个不同且同时存在的JavaScript上下文合并为一个合理的近似单个上下文。或者确定在不引起性能问题的情况下公开所需功能的最佳折衷方案。当您开始寻找函数的恢复程序时,您就会从数据序列化跨越到远程过程调用的领域。
我在编程中想出了一个巧妙的函数,可以将传递给它的对象上的所有属性分类。创建此函数的目的是为了能够在Chrome中传递窗口对象,并根据需要对其进行序列化和反序列化。而且要实现这一点,不使用任何预设的作弊表,像是一个完全愚蠢的检查器,通过用棍子戳传递的值来做出决定。这个函数仅在Chrome中设计和测试过,而且绝不是生产代码,但它是一个很酷的例子。
// categorizeEverything takes any object and will sort its properties into high level categories
// based on it's profile in terms of what it can in JavaScript land. It accomplishes this task with a bafflingly
// small amount of actual code by being extraordinarily uncareful, forcing errors, and generally just
// throwing caution to the wind. But it does a really good job (in the one browser I made it for, Chrome,
// and mostly works in webkit, and could work in Firefox with a modicum of effort)
//
// This will work on any object but its primarily useful for sorting the shitstorm that
// is the webkit global context into something sane.
function categorizeEverything(container){
    var types = {
        // DOMPrototypes are functions that get angry when you dare call them because IDL is dumb.
        // There's a few DOM protos that actually have useful constructors and there currently is no check.
        // They all end up under Class which isn't a bad place for them depending on your goals.
        // [Audio, Image, Option] are the only actual HTML DOM prototypes that sneak by.
        DOMPrototypes: {},
        // Plain object isn't callable, Object is its [[proto]]
        PlainObjects: {},
        // Classes have a constructor
        Classes: {},
        // Methods don't have a "prototype" property and  their [[proto]]  is named "Empty"
        Methods: {},
        // Natives also have "Empty" as their [[proto]]. This list has the big boys:
        // the various Error constructors, Object, Array, Function, Date, Number, String, etc.
        Natives: {},
        // Primitives are instances of String, Number, and Boolean plus bonus friends null, undefined, NaN, Infinity
        Primitives: {}
    };

    var str = ({}).toString;
    function __class__(obj){ return str.call(obj).slice(8,-1); }

    Object.getOwnPropertyNames(container).forEach(function(prop){
        var XX = container[prop],
            xClass = __class__(XX);
        // dumping the various references to window up front and also undefineds for laziness
        if(xClass == "Undefined" || xClass == "global") return;

        // Easy way to rustle out primitives right off the bat,
        // forcing errors for fun and profit.
        try {
            Object.keys(XX);
        } catch(e) {
            if(e.type == "obj_ctor_property_non_object")
                return types.Primitives[prop] = XX;
        }

        // I'm making a LOT flagrant assumptions here but process of elimination is key.
        var isCtor = "prototype" in XX;
        var proto = Object.getPrototypeOf(XX);

        // All Natives also fit the Class category, but they have a special place in our heart.
        if(isCtor && proto.name == "Empty" ||
           XX.name == "ArrayBuffer" ||
           XX.name == "DataView"    ||
           "BYTES_PER_ELEMENT" in XX) {
                return types.Natives[prop] = XX;
        }

        if(xClass == "Function"){
            try {
                // Calling every single function in the global context without a care in the world?
                // There's no way this can end badly.
                // TODO: do this nonsense in an iframe or something
                XX();
            } catch(e){
                // Magical functions which you can never call. That's useful.
                if(e.message == "Illegal constructor"){
                    return types.DOMPrototypes[prop] = XX;
                }
            }

            // By process of elimination only regular functions can still be hanging out
            if(!isCtor) {
                return types.Methods[prop] = XX;
            }
        }

        // Only left with full fledged objects now. Invokability (constructor) splits this group in half
        return (isCtor ? types.Classes : types.PlainObjects)[prop] = XX;

        // JSON, Math, document, and other stuff gets classified as plain objects
        // but they all seem correct going by what their actual profiles and functionality
    });
    return types;
};

23
几乎和离题一样可怕。 - ACK_stoverflow
@benvie 这是否适用于我的问题 - Kragalon

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