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个回答

5
这是我创建的最快的方法,它不使用原型,因此可以在新对象中保留hasOwnProperty。
解决方案是迭代原始对象的顶级属性,制作两个副本,从原始对象中删除每个属性,然后重置原始对象并返回新副本。它只需要迭代与顶级属性相同的次数。这样可以节省检查每个属性是否为函数、对象、字符串等的所有if条件,并且不必迭代每个后代属性。
唯一的缺点是必须提供原始创建命名空间的原始对象才能重置它。
copyDeleteAndReset:function(namespace,strObjName){
    var obj = namespace[strObjName],
    objNew = {},objOrig = {};
    for(i in obj){
        if(obj.hasOwnProperty(i)){
            objNew[i] = objOrig[i] = obj[i];
            delete obj[i];
        }
    }
    namespace[strObjName] = objOrig;
    return objNew;
}

var namespace = {};
namespace.objOrig = {
    '0':{
        innerObj:{a:0,b:1,c:2}
    }
}

var objNew = copyDeleteAndReset(namespace,'objOrig');
objNew['0'] = 'NEW VALUE';

console.log(objNew['0']) === 'NEW VALUE';
console.log(namespace.objOrig['0']) === innerObj:{a:0,b:1,c:2};

这是一个浅拷贝,对于数组无效。它还在全局作用域中声明了i - zkldi

5

有很多方法可以实现这一点,但如果您想不使用任何库来完成此操作,可以使用以下方法:

const cloneObject = (oldObject) => {
  let newObject = oldObject;
  if (oldObject && typeof oldObject === 'object') {
    if(Array.isArray(oldObject)) {
      newObject = [];
    } else if (Object.prototype.toString.call(oldObject) === '[object Date]' && !isNaN(oldObject)) {
      newObject = new Date(oldObject.getTime());
    } else {
      newObject = {};
      for (let i in oldObject) {
        newObject[i] = cloneObject(oldObject[i]);
      }
    }

  }
  return newObject;
}

请告诉我您的想法。


5

我通常使用var newObj = JSON.parse( JSON.stringify(oldObje) );,但这里有一种更合适的方法:

var o = {};

var oo = Object.create(o);

(o === oo); // => false

请注意旧版浏览器!


第二种方法需要一个原型,我更喜欢第一种方法,即使它在性能上不是最好的,因为你可以在许多浏览器和Node JS中使用。 - Hola Soy Edu Feliz Navidad
这很酷,但假设o有一个属性a。现在oo.hasOwnProperty('a')吗? - user420667
不 -- o本质上被添加为oo的原型。这可能不是期望的行为,这就是为什么我编写的99.9%的serialize()方法使用上面提到的JSON方法。我基本上总是使用JSON,并且在使用Object.create时会暴露其他注意事项。 - Cody
1
不,看这段代码!Object.create并不一定会创建一个对象的副本,而是使用旧对象作为克隆的原型。 - 16kb

5
有很多答案,但都没有达到我所需的效果。我想利用jQuery深拷贝的功能... 然而,当它遇到数组时,它只是复制对数组的引用并深度复制其中的项。为了解决这个问题,我写了一个漂亮的递归函数,可以自动创建一个新数组。
(如果需要,它甚至会检查kendo.data.ObservableArray!不过,请确保您调用kendo.observable(newItem),以便再次使数组可观测。)
因此,要完全复制现有项目,您只需执行以下操作:
var newItem = jQuery.extend(true, {}, oldItem);
createNewArrays(newItem);


function createNewArrays(obj) {
    for (var prop in obj) {
        if ((kendo != null && obj[prop] instanceof kendo.data.ObservableArray) || obj[prop] instanceof Array) {
            var copy = [];
            $.each(obj[prop], function (i, item) {
                var newChild = $.extend(true, {}, item);
                createNewArrays(newChild);
                copy.push(newChild);
            });
            obj[prop] = copy;
        }
    }
}

5

Object.assign({},sourceObj) 只有在属性不具备引用类型键时才会克隆对象。 例如:

obj={a:"lol",b:["yes","no","maybe"]}
clonedObj = Object.assign({},obj);

clonedObj.b.push("skip")// changes will reflected to the actual obj as well because of its reference type.
obj.b //will also console => yes,no,maybe,skip

所以,使用此方法无法实现深层克隆。

最好的解决方案是:

var obj = Json.stringify(yourSourceObj)
var cloned = Json.parse(obj);

远非“最佳”。或许适用于简单对象。 - vsync
这个答案不正确 - Object.assign() 是浅拷贝,而且使用 Json 不是使用 JSON 工具的正确方式。 - zkldi

4

浏览了这篇长长的答案列表,几乎所有的解决方案都已经覆盖了,除了我知道的其中一种方法。这是 VANILLA JS 深度克隆对象的方法列表。

  1. JSON.parse(JSON.stringify( obj ) );

  2. 通过 pushState 或 replaceState 使用 history.state

  3. Web 通知 API 但这有一个缺点,需要向用户请求权限。

  4. 通过自己的递归循环来复制每个级别的对象。

  5. 我没有看到的答案 -> 使用 ServiceWorkers。页面和 ServiceWorker 脚本之间传递的消息(对象)将是任何对象的深度克隆。


所有这些内容已经在答案或评论中转换过了。如果您为每个代码示例提供独特的示例,我会给它投票的。 - Jack G

4
希望这能帮到你。
function deepClone(obj) {
    /*
     * Duplicates an object 
     */

    var ret = null;
    if (obj !== Object(obj)) { // primitive types
        return obj;
    }
    if (obj instanceof String || obj instanceof Number || obj instanceof Boolean) { // string objecs
        ret = obj; // for ex: obj = new String("Spidergap")
    } else if (obj instanceof Date) { // date
        ret = new obj.constructor();
    } else
        ret = Object.create(obj.constructor.prototype);

    var prop = null;
    var allProps = Object.getOwnPropertyNames(obj); //gets non enumerables also


    var props = {};
    for (var i in allProps) {
        prop = allProps[i];
        props[prop] = false;
    }

    for (i in obj) {
        props[i] = i;
    }

    //now props contain both enums and non enums 
    var propDescriptor = null;
    var newPropVal = null; // value of the property in new object
    for (i in props) {
        prop = obj[i];
        propDescriptor = Object.getOwnPropertyDescriptor(obj, i);

        if (Array.isArray(prop)) { //not backward compatible
            prop = prop.slice(); // to copy the array
        } else
        if (prop instanceof Date == true) {
            prop = new prop.constructor();
        } else
        if (prop instanceof Object == true) {
            if (prop instanceof Function == true) { // function
                if (!Function.prototype.clone) {
                    Function.prototype.clone = function() {
                        var that = this;
                        var temp = function tmp() {
                            return that.apply(this, arguments);
                        };
                        for (var ky in this) {
                            temp[ky] = this[ky];
                        }
                        return temp;
                    }
                }
                prop = prop.clone();

            } else // normal object 
            {
                prop = deepClone(prop);
            }

        }

        newPropVal = {
            value: prop
        };
        if (propDescriptor) {
            /*
             * If property descriptors are there, they must be copied
             */
            newPropVal.enumerable = propDescriptor.enumerable;
            newPropVal.writable = propDescriptor.writable;

        }
        if (!ret.hasOwnProperty(i)) // when String or other predefined objects
            Object.defineProperty(ret, i, newPropVal); // non enumerable

    }
    return ret;
}

https://github.com/jinujd/Javascript-Deep-Clone


4
这是我的对象克隆器版本。这是jQuery方法的独立版本,只需进行一些微调和调整即可。请查看fiddle。 我用了很多jQuery,直到有一天我意识到我大部分时间只使用这个函数 x_x。
用法与jQuery API中描述的相同:
  • 非深度克隆:extend(object_dest, object_source);
  • 深度克隆:extend(true, object_dest, object_source);
还使用了一个额外的函数来定义是否适合克隆对象。
/**
 * This is a quasi clone of jQuery's extend() function.
 * by Romain WEEGER for wJs library - www.wexample.com
 * @returns {*|{}}
 */
function extend() {
    // Make a copy of arguments to avoid JavaScript inspector hints.
    var to_add, name, copy_is_array, clone,

    // The target object who receive parameters
    // form other objects.
    target = arguments[0] || {},

    // Index of first argument to mix to target.
    i = 1,

    // Mix target with all function arguments.
    length = arguments.length,

    // Define if we merge object recursively.
    deep = false;

    // Handle a deep copy situation.
    if (typeof target === 'boolean') {
        deep = target;

        // Skip the boolean and the target.
        target = arguments[ i ] || {};

        // Use next object as first added.
        i++;
    }

    // Handle case when target is a string or something (possible in deep copy)
    if (typeof target !== 'object' && typeof target !== 'function') {
        target = {};
    }

    // Loop trough arguments.
    for (false; i < length; i += 1) {

        // Only deal with non-null/undefined values
        if ((to_add = arguments[ i ]) !== null) {

            // Extend the base object.
            for (name in to_add) {

                // We do not wrap for loop into hasOwnProperty,
                // to access to all values of object.
                // Prevent never-ending loop.
                if (target === to_add[name]) {
                    continue;
                }

                // Recurse if we're merging plain objects or arrays.
                if (deep && to_add[name] && (is_plain_object(to_add[name]) || (copy_is_array = Array.isArray(to_add[name])))) {
                    if (copy_is_array) {
                        copy_is_array = false;
                        clone = target[name] && Array.isArray(target[name]) ? target[name] : [];
                    }
                    else {
                        clone = target[name] && is_plain_object(target[name]) ? target[name] : {};
                    }

                    // Never move original objects, clone them.
                    target[name] = extend(deep, clone, to_add[name]);
                }

                // Don't bring in undefined values.
                else if (to_add[name] !== undefined) {
                    target[name] = to_add[name];
                }
            }
        }
    }
    return target;
}

/**
 * Check to see if an object is a plain object
 * (created using "{}" or "new Object").
 * Forked from jQuery.
 * @param obj
 * @returns {boolean}
 */
function is_plain_object(obj) {
    // Not plain objects:
    // - Any object or value whose internal [[Class]] property is not "[object Object]"
    // - DOM nodes
    // - window
    if (obj === null || typeof obj !== "object" || obj.nodeType || (obj !== null && obj === obj.window)) {
        return false;
    }
    // Support: Firefox <20
    // The try/catch suppresses exceptions thrown when attempting to access
    // the "constructor" property of certain host objects, i.e. |window.location|
    // https://bugzilla.mozilla.org/show_bug.cgi?id=814622
    try {
        if (obj.constructor && !this.hasOwnProperty.call(obj.constructor.prototype, "isPrototypeOf")) {
            return false;
        }
    }
    catch (e) {
        return false;
    }

    // If the function hasn't returned already, we're confident that
    // |obj| is a plain object, created by {} or constructed with new Object
    return true;
}

1
当测试 if (target === to_add[name]) { continue; } 时,您可能希望添加 || typeof target[name] !== "undefined",以避免覆盖 target 中已有的成员。例如 var a={hello:"world", foo:"bar"}; var b={hello:"you"}; extend(b, a); 我们期望找到 b => {hello:"you", foo:"bar"},但是使用您的代码我们会发现:b => {hello:"world", foo:"bar"} - AymKdn
在我的情况下,我确实希望覆盖现有成员,因此当前的行为对于这种用法来说是正确的。但是感谢您提供这个有用的建议。 - weeger

4

以下是使用 ES2015 默认值和展开运算符进行深克隆对象的方法:

 const makeDeepCopy = (obj, copy = {}) => {
  for (let item in obj) {
    if (typeof obj[item] === 'object') {
      makeDeepCopy(obj[item], copy)
    }
    if (obj.hasOwnProperty(item)) {
      copy = {
        ...obj
      }
    }
  }
  return copy
}

const testObj = {
  "type": "object",
  "properties": {
    "userId": {
      "type": "string",
      "chance": "guid"
    },
    "emailAddr": {
      "type": "string",
      "chance": {
        "email": {
          "domain": "fake.com"
        }
      },
      "pattern": ".+@fake.com"
    }
  },
  "required": [
    "userId",
    "emailAddr"
  ]
}

const makeDeepCopy = (obj, copy = {}) => {
  for (let item in obj) {
    if (typeof obj[item] === 'object') {
      makeDeepCopy(obj[item], copy)
    }
    if (obj.hasOwnProperty(item)) {
      copy = {
        ...obj
      }
    }
  }
  return copy
}

console.log(makeDeepCopy(testObj))


test对象太过简单,它必须包含一些undefined、函数、日期和null,而不仅仅是一堆字符串。 - vsync
无法处理值为空的字段,也无法处理值为数组的字段。 - zkldi

4

我的情况有点不同。我有一个包含嵌套对象和函数的对象。因此,Object.assign()JSON.stringify() 对我的问题都不是解决方案。对我来说,使用第三方库也不是选项。

因此,我决定编写一个简单的函数,使用内置方法来复制一个对象及其字面属性、嵌套对象和函数。

let deepCopy = (target, source) => {
    Object.assign(target, source);
    // check if there's any nested objects
    Object.keys(source).forEach((prop) => {
        /**
          * assign function copies functions and
          * literals (int, strings, etc...)
          * except for objects and arrays, so:
          */
        if (typeof(source[prop]) === 'object') {
            // check if the item is, in fact, an array
            if (Array.isArray(source[prop])) {
                // clear the copied referenece of nested array
                target[prop] = Array();
                // iterate array's item and copy over
                source[prop].forEach((item, index) => {
                    // array's items could be objects too!
                    if (typeof(item) === 'object') {
                        // clear the copied referenece of nested objects
                        target[prop][index] = Object();
                        // and re do the process for nested objects
                        deepCopy(target[prop][index], item);
                    } else {
                        target[prop].push(item);
                    }
                });
            // otherwise, treat it as an object
            } else {
                // clear the copied referenece of nested objects
                target[prop] = Object();
                // and re do the process for nested objects
                deepCopy(target[prop], source[prop]);
            }
        }
    });
};

以下是一个测试代码:

let a = {
    name: 'Human', 
    func: () => {
        console.log('Hi!');
    }, 
    prop: {
        age: 21, 
        info: {
            hasShirt: true, 
            hasHat: false
        }
    },
    mark: [89, 92, { exam: [1, 2, 3] }]
};

let b = Object();

deepCopy(b, a);

a.name = 'Alien';
a.func = () => { console.log('Wassup!'); };
a.prop.age = 1024;
a.prop.info.hasShirt = false;
a.mark[0] = 87;
a.mark[1] = 91;
a.mark[2].exam = [4, 5, 6];

console.log(a); // updated props
console.log(b);

为了提高效率方面的考虑,我认为这是解决我遇到的问题最简单和最有效的方法。如有任何可以使算法更加高效的评论,请不吝赐教。

该代码将对象内的null替换为{},因为typeof null === "object" - zkldi

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