JavaScript中最有效的深度克隆对象的方法是什么?

5167
什么是克隆JavaScript对象最有效的方法?我见过使用obj = eval(uneval(o));,但这是非标准的,只有Firefox支持

我已经尝试过像obj = JSON.parse(JSON.stringify(o));这样的方式,但质疑其效率。

我也看到了递归复制函数的各种缺陷。
我很惊讶没有一个权威的解决方案存在。

566
Eval本身并不可怕,使用不当才是。如果你害怕它的副作用,那么你就没有正确地使用它。你所担心的副作用正是使用Eval的原因。顺便问一句,有人真正回答了你的问题吗? - Tegra Detra
15
复制对象是一个棘手的问题,特别是对于任意集合的自定义对象而言。这可能就是为什么没有现成的方法来完成它的原因。 - b01
12
使用 eval() 通常是不明智的,因为许多JavaScript引擎的优化器必须在处理通过 eval() 设置的变量时关闭。仅仅使用 eval() 就可能导致代码性能更差。 - user56reinstatemonica8
12
请注意,JSON方法会丢失任何在JSON中没有等价的JavaScript类型。例如:JSON.parse(JSON.stringify({a:null,b:NaN,c:Infinity,d:undefined,e:function(){},f:Number,g:false}))将生成{a: null, b: null, c: null, g: false} - oriadam
React社区已经推出了immutability-helper - Navid
67个回答

22

浅拷贝的一行代码 (ECMAScript 第五版):

var origin = { foo : {} };
var copy = Object.keys(origin).reduce(function(c,k){c[k]=origin[k];return c;},{});

console.log(origin, copy);
console.log(origin == copy); // false
console.log(origin.foo == copy.foo); // true

浅拷贝一行代码 (ECMAScript第六版,2015年):

var origin = { foo : {} };
var copy = Object.assign({}, origin);

console.log(origin, copy);
console.log(origin == copy); // false
console.log(origin.foo == copy.foo); // true

这是一个浅拷贝和深度克隆,就像问题所要求的那样。这对于嵌套对象不起作用。 - zkldi

21

目前似乎还没有针对类数组对象的理想深度克隆操作符。如下代码所示,John Resig的jQuery克隆器将具有非数字属性的数组转换为不是数组的对象,而RegDwight的JSON克隆器则会删除非数字属性。以下测试在多个浏览器上说明了这些问题:

function jQueryClone(obj) {
   return jQuery.extend(true, {}, obj)
}

function JSONClone(obj) {
   return JSON.parse(JSON.stringify(obj))
}

var arrayLikeObj = [[1, "a", "b"], [2, "b", "a"]];
arrayLikeObj.names = ["m", "n", "o"];
var JSONCopy = JSONClone(arrayLikeObj);
var jQueryCopy = jQueryClone(arrayLikeObj);

alert("Is arrayLikeObj an array instance?" + (arrayLikeObj instanceof Array) +
      "\nIs the jQueryClone an array instance? " + (jQueryCopy instanceof Array) +
      "\nWhat are the arrayLikeObj names? " + arrayLikeObj.names +
      "\nAnd what are the JSONClone names? " + JSONCopy.names)

19

我没有看到提到 AngularJS,因此认为人们可能想知道...

angular.copy 还提供了一种深拷贝对象和数组的方法。


或者它可以像jQuery扩展一样使用:angular.extend({},obj); - Galvani
2
@Galvani:需要注意的是,jQuery.extendangular.extend都是浅拷贝。而angular.copy则是深拷贝。 - Dan Atkinson

18

只有当您可以使用ECMAScript 6转译器时才能实现。

特点:

  • 复制时不会触发getter/setter。
  • 保留getter/setter。
  • 保留原型信息。
  • 适用于对象字面量函数式OO写作风格。

代码:

function clone(target, source){

    for(let key in source){

        // Use getOwnPropertyDescriptor instead of source[key] to prevent from trigering setter/getter.
        let descriptor = Object.getOwnPropertyDescriptor(source, key);
        if(descriptor.value instanceof String){
            target[key] = new String(descriptor.value);
        }
        else if(descriptor.value instanceof Array){
            target[key] = clone([], descriptor.value);
        }
        else if(descriptor.value instanceof Object){
            let prototype = Reflect.getPrototypeOf(descriptor.value);
            let cloneObject = clone({}, descriptor.value);
            Reflect.setPrototypeOf(cloneObject, prototype);
            target[key] = cloneObject;
        }
        else {
            Object.defineProperty(target, key, descriptor);
        }
    }
    let prototype = Reflect.getPrototypeOf(source);
    Reflect.setPrototypeOf(target, prototype);
    return target;
}

1
对于像Date这样的数据类型存在问题。 - Zortext
如果与具有“null”原型的对象一起使用,这将创建对同一对象实例的引用(_不是_深度复制),因为Object.create(null) instanceof Object为false。 - CherryDT

18

我晚回答了这个问题,但是我有另一种复制对象的方法:

function cloneObject(obj) {
    if (obj === null || typeof(obj) !== 'object')
        return obj;
    var temp = obj.constructor(); // changed
    for (var key in obj) {
        if (Object.prototype.hasOwnProperty.call(obj, key)) {
            obj['isActiveClone'] = null;
            temp[key] = cloneObject(obj[key]);
            delete obj['isActiveClone'];
        }
    }
    return temp;
}

var b = cloneObject({"a":1,"b":2});   // calling

这比以下方式更好、更快:

var a = {"a":1,"b":2};
var b = JSON.parse(JSON.stringify(a));  

var a = {"a":1,"b":2};

// Deep copy
var newObject = jQuery.extend(true, {}, a);

我已经对代码进行了基准测试,您可以在这里测试结果:

并分享结果:enter image description here 参考资料:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty


很有趣,但当我运行你的测试时,它实际上向我展示了方法1是最慢的。 - Antoniossss
和我一样,块1是最低的! - SPG
唯一对我起作用的解决方案!必须深度克隆一个包含其他带有函数属性的对象。完美。 - Phoenix
为什么您要设置 obj['isActiveClone'] = null 然后再将其删除? 而且为什么不调用 obj.hasOwnProperty(key) - Aykut Kllic

18

根据你的目标是否是克隆“普通 JavaScript 对象”,我有两个好的答案。

假设你的意图是创建一个完整的副本,不包含指向源对象的原型引用。如果您不需要完整的克隆,则可以使用其他答案中提供的许多 Object.clone()例程(Crockford 的模式)。

对于普通的 JavaScript 对象,在现代运行时中克隆对象的一种经过验证的好方法是:

var clone = JSON.parse(JSON.stringify(obj));

请注意,源对象必须是一个纯JSON对象。也就是说,它的所有嵌套属性都必须是标量(如布尔值、字符串、数组、对象等)。任何函数或特殊对象,如RegExp或Date都不会被克隆。

这个方法非常有效率。我们尝试了各种克隆方法,这个方法效果最好。我相信有些高手可以想出更快的方法,但我怀疑这只是微小的提升。

这种方法简单易行,易于实现。将其包装成方便的函数,如果您确实需要挤出一些效果,在以后的某个时候再进行优化即可。

对于非纯JavaScript对象,没有一个真正简单的答案。事实上,由于JavaScript函数和内部对象状态的动态性质,这是不可能的。深度克隆具有内部函数的JSON结构需要重新创建这些函数及其内部上下文。而JavaScript根本没有标准化的处理方式。

再次强调,正确的做法是通过声明并重复使用方便的方法来完成。该方便方法可以具备一些关于您自己对象的理解,以便在新对象中正确地重新创建图形。

我们编写了自己的代码,但我看到的最佳通用方法是在这里:

http://davidwalsh.name/javascript-clone

这是正确的想法。作者(David Walsh)已经注释掉了一般化函数的克隆。这取决于您的用例,您可能会选择这样做。

主要思路是您需要根据每种类型特殊处理函数的实例化(或者类原型,可以这么说)。在这里,他提供了一些RegExp和Date的示例。

这段代码不仅简短,而且可读性非常好,很容易扩展。

这有效率吗?当然!既然目标是生成真正的深拷贝克隆,那么您必须遍历源对象图的成员。通过这种方式,您可以微调要处理的子成员以及如何手动处理自定义类型。

两种方法都很有效率。


16

这通常不是最高效的解决方案,但它能满足我的需求。以下是简单的测试案例...

function clone(obj, clones) {
    // Makes a deep copy of 'obj'. Handles cyclic structures by
    // tracking cloned obj's in the 'clones' parameter. Functions 
    // are included, but not cloned. Functions members are cloned.
    var new_obj,
        already_cloned,
        t = typeof obj,
        i = 0,
        l,
        pair; 

    clones = clones || [];

    if (obj === null) {
        return obj;
    }

    if (t === "object" || t === "function") {

        // check to see if we've already cloned obj
        for (i = 0, l = clones.length; i < l; i++) {
            pair = clones[i];
            if (pair[0] === obj) {
                already_cloned = pair[1];
                break;
            }
        }

        if (already_cloned) {
            return already_cloned; 
        } else {
            if (t === "object") { // create new object
                new_obj = new obj.constructor();
            } else { // Just use functions as is
                new_obj = obj;
            }

            clones.push([obj, new_obj]); // keep track of objects we've cloned

            for (key in obj) { // clone object members
                if (obj.hasOwnProperty(key)) {
                    new_obj[key] = clone(obj[key], clones);
                }
            }
        }
    }
    return new_obj || obj;
}

循环数组测试...

a = []
a.push("b", "c", a)
aa = clone(a)
aa === a //=> false
aa[2] === a //=> false
aa[2] === a[2] //=> false
aa[2] === aa //=> true

功能测试...

f = new Function
f.a = a
ff = clone(f)
ff === f //=> true
ff.a === a //=> false

16

对于想要使用JSON.parse(JSON.stringify(obj))版本,但不想丢失日期对象的人,可以使用parse方法的第二个参数将字符串转换回日期:参见此处

function clone(obj) {
  var regExp = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
  return JSON.parse(JSON.stringify(obj), function(k, v) {
    if (typeof v === 'string' && regExp.test(v))
      return new Date(v)
    return v;
  })
}

// usage:
var original = {
 a: [1, null, undefined, 0, {a:null}, new Date()],
 b: {
   c(){ return 0 }
 }
}

var cloned = clone(original)

console.log(cloned)


并非完全克隆 - vsync

14
我不同意得票最高的答案这里。使用递归深克隆比提到的JSON.parse(JSON.stringify(obj))方法要快得多

以下是快速参考的函数:

function cloneDeep (o) {
  let newO
  let i

  if (typeof o !== 'object') return o

  if (!o) return o

  if (Object.prototype.toString.apply(o) === '[object Array]') {
    newO = []
    for (i = 0; i < o.length; i += 1) {
      newO[i] = cloneDeep(o[i])
    }
    return newO
  }

  newO = {}
  for (i in o) {
    if (o.hasOwnProperty(i)) {
      newO[i] = cloneDeep(o[i])
    }
  }
  return newO
}

2
我喜欢这种方法,但它不能正确处理日期;考虑在检查 null 后添加类似 if(o instanceof Date) return new Date(o.valueOf()); 的内容。 - Luis
循环引用时崩溃。 - Harry
在最新的稳定版Firefox中,与那个Jsben.ch链接中的其他策略相比,这种方法要长得多,数量级或更多。它的性能比其他方法更糟糕。 - WBT

13
// obj target object, vals source object
var setVals = function (obj, vals) {
    if (obj && vals) {
        for (var x in vals) {
            if (vals.hasOwnProperty(x)) {
                if (obj[x] && typeof vals[x] === 'object') {
                    obj[x] = setVals(obj[x], vals[x]);
                } else {
                    obj[x] = vals[x];
                }
            }
        }
    }
    return obj;
};

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