如何正确克隆 JavaScript 对象?

我有一个对象x 。我想将其复制为对象y ,以使对y更改不会修改x 。我意识到,复制从内置 JavaScript 对象派生的对象将导致额外的不需要的属性。这不是问题,因为我正在复制自己的文字构造对象之一。

如何正确克隆 JavaScript 对象?

答案

在 JavaScript 中对任何对象执行此操作都不是简单或直接的。您将遇到错误地从对象的原型中选取应保留在原型中而不应复制到新实例的属性的问题。例如,如果要将clone方法添加到Object.prototype ,如一些答案所示,则需要显式跳过该属性。但是,如果还有其他您不知道的其他方法添加到Object.prototype或其他中间原型,该怎么办?在这种情况下,您将复制不应复制的属性,因此需要使用hasOwnProperty方法检测无法预见的非本地属性。

除了不可枚举的属性外,当您尝试复制具有隐藏属性的对象时,还会遇到更棘手的问题。例如, prototype是函数的隐藏属性。同样,对象的原型使用属性__proto__引用,该属性也被隐藏,并且不会通过在源对象的属性上进行迭代的 for / in 循环进行复制。我认为__proto__可能特定于 Firefox 的 JavaScript 解释器,并且在其他浏览器中可能有所不同,但是您可以理解。并不是所有的东西都可以枚举。如果知道隐藏属性的名称,则可以复制它,但是我不知道有什么方法可以自动发现它。

寻求优雅解决方案的另一个障碍是正确设置原型继承的问题。如果源对象的原型为Object ,然后简单地创建一个新的通用对象{}的工作,但如果源的原型是一些后裔Object ,那么你将要丢失从原型您使用跳过了其他成员hasOwnProperty过滤器,或在原型中,但首先不能枚举。一种解决方案可能是调用源对象的constructor属性来获取初始复制对象,然后在属性上进行复制,但是这样您仍然不会获得不可枚举的属性。例如, Date对象将其数据存储为隐藏成员:

function clone(obj) {
    if (null == obj || "object" != typeof obj) return obj;
    var copy = obj.constructor();
    for (var attr in obj) {
        if (obj.hasOwnProperty(attr)) copy[attr] = obj[attr];
    }
    return copy;
}

var d1 = new Date();

/* Executes function after 5 seconds. */
setTimeout(function(){
    var d2 = clone(d1);
    alert("d1 = " + d1.toString() + "\nd2 = " + d2.toString());
}, 5000);

d1的日期字符串将比d2的日期字符串晚 5 秒。使一个Date与另一个Date相同的方法是调用setTime方法,但这特定于Date类。尽管我很乐意弄错,但我认为没有解决该问题的通用解决方案!

当我不得不实施常规的深层复制时,以假定只复制一个简单的ObjectArrayDateStringNumberBoolean来折衷。后三种类型是不可变的,因此我可以执行浅表复制,而不必担心它会更改。我进一步假设ObjectArray包含的任何元素也将是该列表中 6 个简单类型之一。这可以通过以下代码来完成:

function clone(obj) {
    var copy;

    // Handle the 3 simple types, and null or undefined
    if (null == obj || "object" != typeof obj) return obj;

    // Handle Date
    if (obj instanceof Date) {
        copy = new Date();
        copy.setTime(obj.getTime());
        return copy;
    }

    // Handle Array
    if (obj instanceof Array) {
        copy = [];
        for (var i = 0, len = obj.length; i < len; i++) {
            copy[i] = clone(obj[i]);
        }
        return copy;
    }

    // Handle Object
    if (obj instanceof Object) {
        copy = {};
        for (var attr in obj) {
            if (obj.hasOwnProperty(attr)) copy[attr] = clone(obj[attr]);
        }
        return copy;
    }

    throw new Error("Unable to copy obj! Its type isn't supported.");
}

只要对象和数组中的数据形成树形结构,上述功能就可以对我提到的 6 种简单类型充分发挥作用。也就是说,对象中对同一数据的引用不止一个。例如:

// This would be cloneable:
var tree = {
    "left"  : { "left" : null, "right" : null, "data" : 3 },
    "right" : null,
    "data"  : 8
};

// This would kind-of work, but you would get 2 copies of the 
// inner node instead of 2 references to the same copy
var directedAcylicGraph = {
    "left"  : { "left" : null, "right" : null, "data" : 3 },
    "data"  : 8
};
directedAcyclicGraph["right"] = directedAcyclicGraph["left"];

// Cloning this would cause a stack overflow due to infinite recursion:
var cyclicGraph = {
    "left"  : { "left" : null, "right" : null, "data" : 3 },
    "data"  : 8
};
cyclicGraph["right"] = cyclicGraph;

它将无法处理任何 JavaScript 对象,但只要您不认为它会对您扔给它的任何东西起作用,它就可以满足许多目的。

如果您没有在对象内使用Date ,函数,未定义,regExp 或 Infinity,那么一个非常简单的衬里就是JSON.parse(JSON.stringify(object))

const a = {
  string: 'string',
  number: 123,
  bool: false,
  nul: null,
  date: new Date(),  // stringified
  undef: undefined,  // lost
  inf: Infinity,  // forced to 'null'
}
console.log(a);
console.log(typeof a.date);  // Date object
const clone = JSON.parse(JSON.stringify(a));
console.log(clone);
console.log(typeof clone.date);  // result of .toISOString()

这适用于包含对象,数组,字符串,布尔值和数字的所有类型的对象。

另请参阅本文有关浏览器的结构化克隆算法的文章,该算法在与工作人员之间发布消息时使用。它还包含用于深度克隆的功能。

使用 jQuery,您可以使用extend进行浅表复制

var copiedObject = jQuery.extend({}, originalObject)

随后对copiedObject更改将不会影响originalObject ,反之亦然。

或进行深复制

var copiedObject = jQuery.extend(true, {}, originalObject)

在 ECMAScript 6 中,存在Object.assign方法,该方法将所有可枚举的自身属性的值从一个对象复制到另一个对象。例如:

var x = {myProp: "value"};
var y = Object.assign({}, x);

但是请注意,嵌套对象仍会复制为引用。

每个MDN

  • 如果要浅拷贝,请使用Object.assign({}, a)
  • 对于 “深层” 副本,请使用JSON.parse(JSON.stringify(a))

不需要外部库,但您需要首先检查浏览器的兼容性

有很多答案,但是没有一个提到 ECMAScript 5 中的Object.create ,它当然不能提供确切的副本,但是会将源设置为新对象的原型。

因此,这不是对该问题的确切答案,而是单线解决方案,因此很优雅。它最适合 2 种情况:

  1. 此类继承有用的地方(du!)
  2. 不会修改源对象的地方,因此这两个对象之间的关系就不成问题了。

例:

var foo = { a : 1 };
var bar = Object.create(foo);
foo.a; // 1
bar.a; // 1
foo.a = 2;
bar.a; // 2 - prototype changed
bar.a = 3;
foo.a; // Still 2, since setting bar.a makes it an "own" property

为什么我认为此解决方案更好?它是本机的,因此没有循环,没有递归。但是,较旧的浏览器将需要使用 polyfill。

在一行代码中克隆 Javascript 对象的一种优雅方法

Object.assign方法是 ECMAScript 2015(ES6)标准的一部分,可以完全满足您的需求。

var clone = Object.assign({}, obj);

Object.assign()方法用于将所有可枚举的自身属性的值从一个或多个源对象复制到目标对象。

阅读更多...

支持旧版浏览器的polyfill

if (!Object.assign) {
  Object.defineProperty(Object, 'assign', {
    enumerable: false,
    configurable: true,
    writable: true,
    value: function(target) {
      'use strict';
      if (target === undefined || target === null) {
        throw new TypeError('Cannot convert first argument to object');
      }

      var to = Object(target);
      for (var i = 1; i < arguments.length; i++) {
        var nextSource = arguments[i];
        if (nextSource === undefined || nextSource === null) {
          continue;
        }
        nextSource = Object(nextSource);

        var keysArray = Object.keys(nextSource);
        for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
          var nextKey = keysArray[nextIndex];
          var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
          if (desc !== undefined && desc.enumerable) {
            to[nextKey] = nextSource[nextKey];
          }
        }
      }
      return to;
    }
  });
}

互联网上的大多数解决方案都有几个问题。因此,我决定进行跟进,包括为什么不接受接受的答案。

起始情况

我想复制一个 Javascript Object及其所有子Object及其子Object ,依此类推。但是由于我不是普通的开发人员,所以我的Object具有正常的 propertiescircular structures甚至nested objects

因此,让我们首先创建一个circular structure和一个nested object

function Circ() {
    this.me = this;
}

function Nested(y) {
    this.y = y;
}

让我们将所有内容整合到一个名为aObject

var a = {
    x: 'a',
    circ: new Circ(),
    nested: new Nested('a')
};

接下来,我们要将a复制到名为b的变量中并对其进行突变。

var b = a;

b.x = 'b';
b.nested.y = 'b';

您知道这里发生了什么,因为如果没有,您甚至不会落在这个伟大的问题上。

console.log(a, b);

a --> Object {
    x: "b",
    circ: Circ {
        me: Circ { ... }
    },
    nested: Nested {
        y: "b"
    }
}

b --> Object {
    x: "b",
    circ: Circ {
        me: Circ { ... }
    },
    nested: Nested {
        y: "b"
    }
}

现在让我们找到一个解决方案。

JSON 格式

我尝试的第一次尝试是使用JSON

var b = JSON.parse( JSON.stringify( a ) );

b.x = 'b';
b.nested.y = 'b';

不要浪费太多时间,您会得到TypeError: Converting circular structure to JSON

递归副本(可接受的 “答案”)

让我们看一下接受的答案。

function cloneSO(obj) {
    // Handle the 3 simple types, and null or undefined
    if (null == obj || "object" != typeof obj) return obj;

    // Handle Date
    if (obj instanceof Date) {
        var copy = new Date();
        copy.setTime(obj.getTime());
        return copy;
    }

    // Handle Array
    if (obj instanceof Array) {
        var copy = [];
        for (var i = 0, len = obj.length; i < len; i++) {
            copy[i] = cloneSO(obj[i]);
        }
        return copy;
    }

    // Handle Object
    if (obj instanceof Object) {
        var copy = {};
        for (var attr in obj) {
            if (obj.hasOwnProperty(attr)) copy[attr] = cloneSO(obj[attr]);
        }
        return copy;
    }

    throw new Error("Unable to copy obj! Its type isn't supported.");
}

看起来不错吧?它是对象的递归副本,还可以处理其他类型,例如Date ,但这不是必需的。

var b = cloneSO(a);

b.x = 'b';
b.nested.y = 'b';

递归和circular structures不能很好地协同工作... RangeError: Maximum call stack size exceeded

本机解决方案

与我的同事吵架后,我的老板问我们发生了什么,经过一番谷歌搜索,他找到了一个简单的解决方案 。它称为Object.create

var b = Object.create(a);

b.x = 'b';
b.nested.y = 'b';

该解决方案是在一段时间之前添加到 Javascript 的,甚至可以处理circular structure

console.log(a, b);

a --> Object {
    x: "a",
    circ: Circ {
        me: Circ { ... }
    },
    nested: Nested {
        y: "b"
    }
}

b --> Object {
    x: "b",
    circ: Circ {
        me: Circ { ... }
    },
    nested: Nested {
        y: "b"
    }
}

... 您会看到,它不适用于内部的嵌套结构。

用于本机解决方案的 polyfill

就像 IE 8 一样,在较旧的浏览器中也有一个针对Object.create的 polyfill。这有点像 Mozilla 的建议,当然,它并不完美,并且会导致与本机解决方案相同的问题。

function F() {};
function clonePF(o) {
    F.prototype = o;
    return new F();
}

var b = clonePF(a);

b.x = 'b';
b.nested.y = 'b';

我将F放在范围之外,因此我们可以看一下instanceof告诉我们什么。

console.log(a, b);

a --> Object {
    x: "a",
    circ: Circ {
        me: Circ { ... }
    },
    nested: Nested {
        y: "b"
    }
}

b --> F {
    x: "b",
    circ: Circ {
        me: Circ { ... }
    },
    nested: Nested {
        y: "b"
    }
}

console.log(typeof a, typeof b);

a --> object
b --> object

console.log(a instanceof Object, b instanceof Object);

a --> true
b --> true

console.log(a instanceof F, b instanceof F);

a --> false
b --> true

与本机解决方案相同的问题,但输出会差一些。

更好(但不是完美)的解决方案

进行深入研究时,我发现了与此类似的问题( 在 Javascript 中,当执行深度复制时,由于属性为 “this”,如何避免循环? ),但有一个更好的解决方案。

function cloneDR(o) {
    const gdcc = "__getDeepCircularCopy__";
    if (o !== Object(o)) {
        return o; // primitive value
    }

    var set = gdcc in o,
        cache = o[gdcc],
        result;
    if (set && typeof cache == "function") {
        return cache();
    }
    // else
    o[gdcc] = function() { return result; }; // overwrite
    if (o instanceof Array) {
        result = [];
        for (var i=0; i<o.length; i++) {
            result[i] = cloneDR(o[i]);
        }
    } else {
        result = {};
        for (var prop in o)
            if (prop != gdcc)
                result[prop] = cloneDR(o[prop]);
            else if (set)
                result[prop] = cloneDR(cache);
    }
    if (set) {
        o[gdcc] = cache; // reset
    } else {
        delete o[gdcc]; // unset again
    }
    return result;
}

var b = cloneDR(a);

b.x = 'b';
b.nested.y = 'b';

让我们看一下输出...

console.log(a, b);

a --> Object {
    x: "a",
    circ: Object {
        me: Object { ... }
    },
    nested: Object {
        y: "a"
    }
}

b --> Object {
    x: "b",
    circ: Object {
        me: Object { ... }
    },
    nested: Object {
        y: "b"
    }
}

console.log(typeof a, typeof b);

a --> object
b --> object

console.log(a instanceof Object, b instanceof Object);

a --> true
b --> true

console.log(a instanceof F, b instanceof F);

a --> false
b --> false

满足了这些要求,但是仍然存在一些较小的问题,包括将nestedcircinstance更改为Object

共享叶子的树的结构不会被复制,它们将成为两个独立的叶子:

[Object]                     [Object]
         /    \                       /    \
        /      \                     /      \
      |/_      _\|                 |/_      _\|  
  [Object]    [Object]   ===>  [Object]    [Object]
       \        /                 |           |
        \      /                  |           |
        _\|  |/_                 \|/         \|/
        [Object]               [Object]    [Object]

结论

最后一种使用递归和缓存的解决方案可能不是最好的,但它是对象的真实副本。它处理简单的propertiescircular structuresnested object ,但在克隆时会弄乱它们的实例。

jsfiddle

如果您可以使用浅表副本,那么 underscore.js 库提供了一个clone方法。

y = _.clone(x);

或者你可以像扩展它

copiedObject = _.extend({},originalObject);

好的,假设您在下面有这个对象并且想要克隆它:

let obj = {a:1, b:2, c:3}; //ES6

要么

var obj = {a:1, b:2, c:3}; //ES5

答案主要取决于您使用的ECMAscript ,在ES6+ ,您可以简单地使用Object.assign进行克隆:

let cloned = Object.assign({}, obj); //new {a:1, b:2, c:3};

或使用像这样的传播算子:

let cloned = {...obj}; //new {a:1, b:2, c:3};

但是,如果使用ES5 ,则可以使用几种方法,但可以使用JSON.stringify ,只需确保不使用大量数据进行复制即可,但是在许多情况下,它可能是一种方便的方式,例如:

let cloned = JSON.parse(JSON.stringify(obj)); 
//new {a:1, b:2, c:3};, can be handy, but avoid using on big chunk of data over and over