如何正确克隆一个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个回答

5

使用(...)进行对象复制

//bad
const original = { a: 1, b: 2 };
const copy = Object.assign({}, original, { c: 3 }); // copy => { a: 1, b: 2,c: 3 }

//good
const originalObj = { id: 5, name: 'San Francisco'};
const copyObject = {...originalObj, pincode: 4444};
console.log(copyObject)  //{ id: 5, name: 'San Francisco', pincode: 4444 }

同样适用于将数组复制从一个数组到另一个数组

const itemsCopy = [...items];

为什么Object.assign方法被标记为“不好”的方式?有人能详细解释一下吗? - MrSegFaulty

4
基于模板克隆一个对象。如果你不想要完全相同的副本,但是你想要某种可靠的克隆操作的健壮性,而且你只想要一些位被克隆或者你想要确保你可以控制每个属性值的存在或格式被克隆,那么该怎么办呢?
我提供这个内容是因为它对我们很有用,而且我们创建它是因为我们找不到类似的东西。你可以使用它来基于指定要克隆的对象的属性的“模板”对象来克隆一个对象,并且模板允许函数将这些属性转换为不同的东西,如果它们不存在于源对象上或者无论你如何处理克隆。如果这不是有用的,我相信有人可以删除这个答案。
   function isFunction(functionToCheck) {
       var getType = {};
       return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
   }

   function cloneObjectByTemplate(obj, tpl, cloneConstructor) {
       if (typeof cloneConstructor === "undefined") {
           cloneConstructor = false;
       }
       if (obj == null || typeof (obj) != 'object') return obj;

       //if we have an array, work through it's contents and apply the template to each item...
       if (Array.isArray(obj)) {
           var ret = [];
           for (var i = 0; i < obj.length; i++) {
               ret.push(cloneObjectByTemplate(obj[i], tpl, cloneConstructor));
           }
           return ret;
       }

       //otherwise we have an object...
       //var temp:any = {}; // obj.constructor(); // we can't call obj.constructor because typescript defines this, so if we are dealing with a typescript object it might reset values.
       var temp = cloneConstructor ? new obj.constructor() : {};

       for (var key in tpl) {
           //if we are provided with a function to determine the value of this property, call it...
           if (isFunction(tpl[key])) {
               temp[key] = tpl[key](obj); //assign the result of the function call, passing in the value
           } else {
               //if our object has this property...
               if (obj[key] != undefined) {
                   if (Array.isArray(obj[key])) {
                       temp[key] = [];
                       for (var i = 0; i < obj[key].length; i++) {
                           temp[key].push(cloneObjectByTemplate(obj[key][i], tpl[key], cloneConstructor));
                       }
                   } else {
                       temp[key] = cloneObjectByTemplate(obj[key], tpl[key], cloneConstructor);
                   }
               }
           }
       }

       return temp;
   }

一个简单的调用方式如下所示:
var source = {
       a: "whatever",
       b: {
           x: "yeah",
           y: "haha"
       }
   };
   var template = {
       a: true, //we want to clone "a"
       b: {
           x: true //we want to clone "b.x" too
       }
   }; 
   var destination = cloneObjectByTemplate(source, template);

如果你想使用一个函数来确保返回一个属性或确保它是一个特定的类型,可以使用以下模板。我们提供了一个函数,而不是使用{ ID: true },这个函数仍然只是复制源对象的ID属性,但它确保即使在源对象中不存在该属性,也会将其转换为数字。

 var template = {
    ID: function (srcObj) {
        if(srcObj.ID == undefined){ return -1; }
        return parseInt(srcObj.ID.toString());
    }
}

数组可以克隆,但如果您想要的话,您也可以编写自己的函数来处理这些单独的属性,并执行类似于以下的特殊操作:

 var template = {
    tags: function (srcObj) {
        var tags = [];
        if (process.tags != undefined) {
            for (var i = 0; i < process.tags.length; i++) {

                tags.push(cloneObjectByTemplate(
                  srcObj.tags[i],
                  { a : true, b : true } //another template for each item in the array
                );
            }
        }
        return tags;
    }
 }

所以在上面的示例中,如果源对象存在tags属性(假设它是一个数组),我们的模板只是复制它,并且针对该数组中的每个元素都调用clone函数来基于第二个模板分别克隆它,该模板只是复制这些标签元素的ab属性。如果你正在将对象带进和带出node.js,并且想要控制克隆这些对象的哪些属性,则这是一种很好的控制方法,而且此代码也适用于浏览器。以下是其使用示例:http://jsfiddle.net/hjchyLt1/

4

虽然这里有一些很棒的链接,但它并不是一个真正的答案。如果扩展到包括引用算法的实现,它可能就成为一个答案了。 - RobG

4

正如此链接所述,使用以下代码:

let clone = Object.create(Object.getPrototypeOf(obj),
 Object.getOwnPropertyDescriptors(obj));

在这里回答问题是很好的,但这是一个旧问题,已经有很多好的答案了。任何新的答案都应该添加重要的新信息和新见解到这个话题中。你在这里的回答非常简短,没有解释,离线页面的链接虽然有用,但如果链接失效,答案应该是自包含的,并且有足够的细节。 - AdrianHHH
这与 Object.assign(newObj, obj) 完全相同,它是浅拷贝。 - Exlord

4

4
根据Airbnb JavaScript Style Guide,该指南有404位贡献者:
优先使用对象展开运算符而非Object.assign进行浅拷贝。使用对象剩余运算符获取省略了某些属性的新对象。
// very bad
const original = { a: 1, b: 2 };
const copy = Object.assign(original, { c: 3 }); // this mutates `original` ಠ_ಠ
delete copy.a; // so does this

// bad
const original = { a: 1, b: 2 };
const copy = Object.assign({}, original, { c: 3 }); // copy => { a: 1, b: 2, c: 3 }

// good
const original = { a: 1, b: 2 };
const copy = { ...original, c: 3 }; // copy => { a: 1, b: 2, c: 3 }

const { a, ...noA } = copy; // noA => { b: 2, c: 3 }

我想提醒你,即使Airbnb极力推荐使用对象展开运算符的方法。请记住,微软Edge浏览器仍不支持这一2018年推出的功能。

ES2016+ 兼容性表 >>


4

简单

var restore = { name:'charlesi',
age:9}
var prev_data ={
name: 'charles'
age : 10
}

var temp = JSON.stringify(prev_data)
restore = JSON.parse(temp)

restore = {
name:'charlie',
age : 12}

输出 prev_data:

{
name: 'charles'
age : 10
} 

3

我认为,使用缓存的循环调用是我们在没有库的情况下可以做到最好的。

而被低估的WeakMap解决了循环引用的问题,在其中存储旧对象和新对象之间对应的引用对,可以帮助我们轻松地重新创建整个树形结构。

我防止了DOM元素的深层克隆,可能你不想克隆整个页面 :)

function deepCopy(object) {
    const cache = new WeakMap(); // Map of old - new references

    function copy(obj) {
        if (typeof obj !== 'object' ||
            obj === null ||
            obj instanceof HTMLElement
        )
            return obj; // primitive value or HTMLElement

        if (obj instanceof Date) 
            return new Date().setTime(obj.getTime());

        if (obj instanceof RegExp) 
            return new RegExp(obj.source, obj.flags);

        if (cache.has(obj)) 
            return cache.get(obj);

        const result = obj instanceof Array ? [] : {};

        cache.set(obj, result); // store reference to object before the recursive starts

        if (obj instanceof Array) {
            for(const o of obj) {
                 result.push(copy(o));
            }
            return result;
        }

        const keys = Object.keys(obj); 

        for (const key of keys)
            result[key] = copy(obj[key]);

        return result;
    }

    return copy(object);
}

一些测试:

// #1
const obj1 = { };
const obj2 = { };
obj1.obj2 = obj2;
obj2.obj1 = obj1; // Trivial circular reference

var copy = deepCopy(obj1);
copy == obj1 // false
copy.obj2 === obj1.obj2 // false
copy.obj2.obj1.obj2 // and so on - no error (correctly cloned).

// #2
const obj = { x: 0 }
const clone = deepCopy({ a: obj, b: obj });
clone.a == clone.b // true

// #3
const arr = [];
arr[0] = arr; // A little bit weird but who cares
clone = deepCopy(arr)
clone == arr // false;
clone[0][0][0][0] == clone // true;

注意:我使用常量、for of循环、=>运算符和WeakMaps来创建更为重要的代码。这种语法(ES6)被当今的浏览器所支持。


3

我已经看过上面的所有解决方案,它们都很好。但是,还有另一种方法可以用来克隆对象(带有值而非引用)。Object.assign

let x = {
    a: '1',
    b: '2'
}

let y = Object.assign({}, x)
y.a = "3"

console.log(x)

输出结果将会是:
{ a: '1', b: '2' }

此外,您也可以使用同样的方法克隆数组。
clonedArray = Object.assign([], array)

3
你可以使用函数闭包来获得深复制的所有好处,而不需要进行深复制。这是一种非常不同的范式,但效果很好。不要试图复制现有对象,只需在需要时使用函数实例化一个新对象即可。
首先,创建一个返回对象的函数。
function template() {
  return {
    values: [1, 2, 3],
    nest: {x: {a: "a", b: "b"}, y: 100}
  };
}

然后创建一个简单的浅拷贝函数。
function copy(a, b) {
  Object.keys(b).forEach(function(key) {
    a[key] = b[key];
  });
}

创建一个新对象,并将模板的属性复制到它上面。
var newObject = {}; 
copy(newObject, template());

但上述复制步骤并非必需。您只需要执行以下操作即可:
var newObject = template();

现在你有了一个新对象,测试一下它的属性:

console.log(Object.keys(newObject));

这会显示:
["values", "nest"]

是的,那些是newObject自己的属性,而不是指向另一个对象属性的引用。让我们来检查一下:

console.log(newObject.nest.x.b);

这将显示:
"b"

新对象已经继承了模板对象的所有属性,但是没有任何依赖关系链。 http://jsbin.com/ISUTIpoC/1/edit?js,console 我添加了这个例子来促进一些讨论,请添加一些评论 :)

1
Object.keys在JavaScript 1.8.5之前没有实现,这意味着它在IE 8和其他旧版浏览器中不可用。因此,虽然这个答案在现代浏览器中运行良好,但在IE 8中会失败。因此,如果您使用此方法,必须使用正确的模拟Object.keys填充程序。 - Blaine Kasten

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