如何正确克隆一个JavaScript对象?

3720
我有一个对象 x。我想将它作为对象 y 复制,使得对 y 的更改不会修改 x。我意识到,复制从内置 JavaScript 对象派生的对象会导致额外的、不必要的属性。这不是问题,因为我要复制的是我自己通过字面量构造的对象。
如何正确地克隆 JavaScript 对象?

34
看这个问题:https://dev59.com/83VD5IYBdhLWcg3wAGiD这个问题是关于如何克隆JavaScript对象的,你需要找到一种高效的方法来实现这个目标。 - Niyaz
289
对于 JSON,我使用 mObj=JSON.parse(JSON.stringify(jsonObject)); - Lord Loh.
78
我不明白为什么没有人建议使用 Object.create(o),它完全满足了作者的要求。 - froginvasion
59
var x = { deep: { key: 1 } }; var y = Object.create(x); x.deep.key = 2; 执行完这段代码后,y.deep.key 的值也会变成2,因此 Object.create 不能用于克隆对象。 - Ruben Stolk
23
@r3wt那样做行不通...请先对解决方案进行基本测试再发帖。 - user3275211
显示剩余21条评论
82个回答

1932

2022更新

有一个名为结构化克隆的新JS标准。它在许多浏览器中都可以使用(请参见Can I Use)。

const clone = structuredClone(object);

旧答案

在JavaScript中,对于任何对象来说,这都不是简单或直接的。您将遇到从对象的原型中错误地挑选出应该留在原型中而不是复制到新实例中的属性的问题。例如,如果您正在为Object.prototype添加一个clone方法,就像某些答案所描述的那样,您需要明确跳过该属性。但是,如果有其他附加方法添加到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);
< p > d1 的日期字符串将比 d2 慢 5 秒。使一个 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对象,但只要您不假设它可以适用于任何您投入其中的内容,它可能对许多目的足够。


这里缺少符号键和符号值。现在,使用Object.getOwnPropertyDescriptors更好。 - Sebastian Simon
2
@JoshuaDavid的更新,目前在所有浏览器中支持率为82.57%。 - Kay Angevare
如果你想使用 structuredClone(object),你需要安装至少 17.0.29 版本的 Node.js。你可以使用以下命令:npm i --save-dev @types/node@17.0.29 - DariusV
2
如果我想要“克隆”对象并且还想要“追加”一些内容,该怎么办? - Azhar Uddin Sheikh
1
在移动设备上使用此函数时遇到了问题。
  • 在iOS 11、12、13、14上,在Safari和Google Chrome上无法正常工作(在iOS 15、16上可以工作)。
  • 在Android 12、11、10、9上,在Samsung Internet浏览器上可以工作(Chrome也可以)。
- jfxninja
显示剩余4条评论

1157
如果您的对象内部没有使用 Date、函数、undefinedRegExpInfinity,那么一个非常简单的一行代码就可以实现深拷贝: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()

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

另请参见有关浏览器结构化克隆算法的文章,当在主线程和工作线程之间发送消息时使用此算法。它还包含一个用于深层复制的函数。


1
有时候,最好的答案就是最简单的。天才。 - dewd
非常有用,但当比较包含其他对象的对象时,在两个完全相等的对象不被认为是相等时,我遇到了意外的行为。使用JSON.stringify(x) == JSON.stringify(JSON.parse(JSON.stringify(a)))来解决此问题。由于某种原因,将其作为字符串进行比较时可以完美地按预期工作,否则无法匹配。 - Agustin L. Lacuara
@AgustinL.Lacuara 在JS中无法比较复杂的数据类型。 a = {}; b = {}; a == b 的结果是 false。但在执行 a = b 后,结果变为 true,因为它不仅相同而且是同一个对象。 - heinob
2
这个工作可以完成,但是这违反了任何良好的编程实践。在巴西,我们称之为“Gambiarra”。 - Diego Favero

836

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

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

但是要注意这是一份浅拷贝 - 嵌套对象仍将作为引用被复制。


814

使用 jQuery,你可以使用 extend 方法进行 浅拷贝

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

对于copiedObject的后续更改不会影响originalObject,反之亦然。

或者进行深层复制

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

329

根据MDN的描述:

  • 如果需要进行浅拷贝,请使用Object.assign({}, a)
  • 如果需要进行“深度”拷贝,请使用JSON.parse(JSON.stringify(a))

不需要使用外部库,但是需要先检查浏览器兼容性


当您在对象中拥有函数时,问题就会发生JSON.parse(JSON.stringify(a))。 - Tosh

149

一行代码实现Javascript对象的优雅克隆

Object.assign方法是ECMAScript 2015(ES6)标准的一部分,正好可以实现您需要的功能。

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

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

阅读更多...

支持旧版浏览器的填充代码

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;
    }
  });
}

55
这只会执行浅层次的“克隆”。 - Marcus Junius Brutus
我吃了苦头,才明白 objA = objB; 会带来各种麻烦。这个方法似乎解决了问题,至少目前是这样的... - WinEunuuchs2Unix

143

有很多答案,但没有一个提到ECMAScript 5中的Object.create,这确实不能给你一个完全相同的副本,但将源设置为新对象的原型。

因此,这不是问题的确切答案,但它是一个简洁的单行解决方案。并且它最适用于两种情况:

  1. 在继承非常有用的情况下(显而易见!)
  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。


116
这是原型继承,不是克隆。这两者完全不同。新对象没有任何自己的属性,它只是指向原型的属性。克隆的目的是创建一个全新的对象,不引用另一个对象中的任何属性。 - d13

100

互联网上的大多数解决方案存在一些问题。因此,我决定做一个后续,其中包括为什么不应该接受已接受的答案。

起始情况

我想要深度复制一个JavaScript Object及其所有子元素、它们的子元素等等。但由于我不是普通开发人员,我的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: 将循环结构转换为 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';

递归和 循环结构 不太兼容... RangeError: Maximum call stack size exceeded

本地解决方案

和同事争论了一番后,老板让我们说明情况,然后通过搜索找到了一个简单的解决方案。它被称为Object.create

var b = Object.create(a);

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

这个解决方案在一段时间前被添加到了Javascript中,甚至可以处理循环结构

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"
    }
}

...你看,它在嵌套结构内部没有起作用。

对原生解决方案的填充

在旧浏览器中(如IE 8)有一个对Object.create的填充。这是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]

结论

最后一个使用递归和缓存的解决方案可能不是最好的,但它是对象的真正深拷贝。它处理简单的属性循环结构嵌套对象,但是在克隆时会弄乱它们的实例。

jsfiddle


12
因此结论是要避免那个问题 :) - mikus
在没有涵盖更多基本用例的“真正”规范之前,是的,@mikus。 - Fabio Poloni
2
以上提供的解决方案的分析还算可以,但作者得出的结论表明这个问题没有解决方案。 - Amir Mog
2
很遗憾,JS没有包含本地克隆函数。 - l00k
1
在所有的顶级答案中,我觉得这个接近正确答案。 - KTU
我正在使用“更好的(但不是完美的)解决方案”,并且在使用Sets时,必须添加一个额外的if语句:if (o instanceof Array) { /*...*/ }:else if (o instanceof Set) { result = new Set(); for (var i of o) { result.add(i); } } - watery

80
如果你可以接受浅复制的话,underscore.js库有一个克隆方法。

如果您不介意使用浅复制,underscore.js库有一个clone方法。

y = _.clone(x);

或者你可以像这样扩展它

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

2
感谢。在 Meteor 服务器上使用这种技术。 - Turbo
要快速开始使用lodash,我建议学习npm、Browserify以及lodash。我成功地使用了“npm i --save lodash.clone”和“var clone = require('lodash.clone');”来克隆。为了让require工作,你需要像browserify这样的东西。一旦你安装并学会了它的工作原理,每次运行代码时,你都会使用“browserify yourfile.js > bundle.js;start chrome index.html”(而不是直接进入Chrome)。这将把你的文件和你从npm模块中所需的所有文件收集到bundle.js中。你可能可以节省时间并自动化这个步骤,使用Gulp。 - user11104582

77

好的, 想象你有下面这个对象并想要克隆它:

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

答案主要取决于你使用的是哪个ECMAscript版本,在ES6+中,你可以简单地使用Object.assign来进行克隆:

答案主要取决于您使用的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

请问您能否举个例子,说明什么是“大块数据”?100kb?100MB?谢谢! - user1063287
是的,@user1063287,基本上数据越大,性能就越差... 所以这真的取决于你要做多少次,而不是 kb、mb 或 gb 的大小... 另外它对于函数和其他东西也不起作用... - Alireza
3
Object.assign实现的是浅拷贝(就像spread操作符、@Alizera一样)。 - Bogdan D
你不能在 ES5 中使用 let :^) @Alireza - Womble

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