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

13

这里是一个全面的clone()方法,可以克隆任何JavaScript对象。它几乎处理了所有情况:

function clone(src, deep) {

    var toString = Object.prototype.toString;
    if (!src && typeof src != "object") {
        // Any non-object (Boolean, String, Number), null, undefined, NaN
        return src;
    }

    // Honor native/custom clone methods
    if (src.clone && toString.call(src.clone) == "[object Function]") {
        return src.clone(deep);
    }

    // DOM elements
    if (src.nodeType && toString.call(src.cloneNode) == "[object Function]") {
        return src.cloneNode(deep);
    }

    // Date
    if (toString.call(src) == "[object Date]") {
        return new Date(src.getTime());
    }

    // RegExp
    if (toString.call(src) == "[object RegExp]") {
        return new RegExp(src);
    }

    // Function
    if (toString.call(src) == "[object Function]") {

        //Wrap in another method to make sure == is not true;
        //Note: Huge performance issue due to closures, comment this :)
        return (function(){
            src.apply(this, arguments);
        });
    }

    var ret, index;
    //Array
    if (toString.call(src) == "[object Array]") {
        //[].slice(0) would soft clone
        ret = src.slice();
        if (deep) {
            index = ret.length;
            while (index--) {
                ret[index] = clone(ret[index], true);
            }
        }
    }
    //Object
    else {
        ret = src.constructor ? new src.constructor() : {};
        for (var prop in src) {
            ret[prop] = deep
                ? clone(src[prop], true)
                : src[prop];
        }
    }
    return ret;
};

它将基本类型转换为包装对象,但在大多数情况下并不是一个好的解决方案。 - Danubian Sailor
@DanubianSailor - 我认为它不会...它似乎从一开始就立即返回原始类型,并且似乎没有对它们进行任何处理,将它们转换为包装对象并返回。 - Jimbo Jonny

12

AngularJS

如果您正在使用Angular,您也可以这样做。

var newObject = angular.copy(oldObject);

12
在JavaScript中,你可以像这样编写你自己的deepCopy方法:
function deepCopy(src) {
  let target = Array.isArray(src) ? [] : {};
  for (let prop in src) {
    let value = src[prop];
    if(value && typeof value === 'object') {
      target[prop] = deepCopy(value);
  } else {
      target[prop] = value;
  }
 }
    return target;
}

1
这个容易受到全局对象污染的影响。如果 (prop === 'constuctor' && typeof src[prop] === 'function') 或者 (prop === '__proto__'),就不应该复制 prop - Frank Fajardo

10

根据我的经验,递归版本明显比JSON.parse(JSON.stringify(obj))效率更高。以下是一种现代化的递归式深拷贝函数,可以放在一行中:

function deepCopy(obj) {
  return Object.keys(obj).reduce((v, d) => Object.assign(v, {
    [d]: (obj[d].constructor === Object) ? deepCopy(obj[d]) : obj[d]
  }), {});
}

这比 JSON.parse... 方法执行速度快了约 40倍


2
伪代码如下:对于每个键,将其值分配给新对象中相同的键(浅拷贝)。但是,如果该值的类型为“Object”(无法进行浅拷贝),则该函数会使用该值作为参数递归调用自身。 - Parabolord
1
很遗憾,当值为数组时它不能正常工作。 但是,修改以使其适用于该情况不应该太困难。 - zenw0lf
类型错误:无法读取未定义的属性“constructor”。 - medBouzid
无法处理 null,因为它尝试访问 null.constructor,也无法处理数组,因为它会将它们转换为对象。 - zkldi

8

7

由于递归在JavaScript中的开销太大,而大多数我找到的答案都使用递归,而JSON方法将跳过无法转换为JSON的部分(例如Function等)。因此,我进行了一些研究,并发现了这种避免递归的跳板技术。以下是代码:

/*
 * Trampoline to avoid recursion in JavaScript, see:
 *     https://www.integralist.co.uk/posts/functional-recursive-javascript-programming/
 */
function trampoline() {
    var func = arguments[0];
    var args = [];
    for (var i = 1; i < arguments.length; i++) {
        args[i - 1] = arguments[i];
    }

    var currentBatch = func.apply(this, args);
    var nextBatch = [];

    while (currentBatch && currentBatch.length > 0) {
        currentBatch.forEach(function(eachFunc) {
            var ret = eachFunc();
            if (ret && ret.length > 0) {
                nextBatch = nextBatch.concat(ret);
            }
        });

        currentBatch = nextBatch;
        nextBatch = [];
    }
};

/*
 *  Deep clone an object using the trampoline technique.
 *
 *  @param target {Object} Object to clone
 *  @return {Object} Cloned object.
 */
function clone(target) {
    if (typeof target !== 'object') {
        return target;
    }
    if (target == null || Object.keys(target).length == 0) {
        return target;
    }

    function _clone(b, a) {
        var nextBatch = [];
        for (var key in b) {
            if (typeof b[key] === 'object' && b[key] !== null) {
                if (b[key] instanceof Array) {
                    a[key] = [];
                }
                else {
                    a[key] = {};
                }
                nextBatch.push(_clone.bind(null, b[key], a[key]));
            }
            else {
                a[key] = b[key];
            }
        }
        return nextBatch;
    };

    var ret = target instanceof Array ? [] : {};
    (trampoline.bind(null, _clone))(target, ret);
    return ret;
};

3
在大多数 JavaScript 实现中,尾调用优化实际上非常高效,并且在 ES6 中需要进行优化。 - rich remer
嗨,我之前做了一个小测试,当目标对象变得复杂时,调用堆栈很容易溢出,虽然我没有记录任何笔记,但希望在es6中这将是一个重大的优化。 - Bodhi Hu
2
栈很容易溢出,可能是由于循环引用导致的。 - Yichong

6

ES 2017的示例:

let objectToCopy = someObj;
let copyOfObject = {};
Object.defineProperties(copyOfObject, Object.getOwnPropertyDescriptors(objectToCopy));
// copyOfObject will now be the same as objectToCopy

谢谢你的回答。我尝试了你的方法,但不幸的是它并没有起作用。因为可能是我的错误,所以我请你检查一下我的 JSFiddle示例 ,如果是我的问题,我会给你点赞的。 - Takeshi Tokugawa YD
当我运行你的fiddle时,我得到了{ foo: 1, bar: { fooBar: 22, fooBaz: 33, fooFoo: 11 }, baz: 3}{ foo: 1, bar: { fooBar: 22, fooBaz: 44, fooFoo: 11 }, baz: 4}。这不是你期望发生的吗? - codeMonkey
你复制的内容是我期望的。我不明白为什么,但我在控制台中看到testObj2testObj3fooBaz: 44都是一样的...(截图 - Takeshi Tokugawa YD
3
这不是深拷贝,而是浅拷贝。@GurebuBokofu - Nikita Malyschkin

6
Lodash有一个函数可以为您处理这个问题,如下所示。
var foo = {a: 'a', b: {c:'d', e: {f: 'g'}}};

var bar = _.cloneDeep(foo);
// bar = {a: 'a', b: {c:'d', e: {f: 'g'}}} 

请点击此处查看文档。


最终我使用了这个,因为JSON.parse(JSON.stringify(obj))无法保留原始对象的原型。 - tommyalvarez
这是我的常用答案。除此之外,我使用 Lodash 的 merge 函数,使得深拷贝和浅拷贝的语法保持一致。 //深拷贝: _.merge({},foo) //浅拷贝: Object.Assign({}, foo) - RobbyD

6

一行ECMAScript 6的解决方案(不处理特殊对象类型,如日期/正则表达式):

const clone = (o) =>
  typeof o === 'object' && o !== null ?      // only clone objects
  (Array.isArray(o) ?                        // if cloning an array
    o.map(e => clone(e)) :                   // clone each of its elements
    Object.keys(o).reduce(                   // otherwise reduce every key in the object
      (r, k) => (r[k] = clone(o[k]), r), {}  // and save its cloned value into a new object
    )
  ) :
  o;                                         // return non-objects as is

var x = {
  nested: {
    name: 'test'
  }
};

var y = clone(x);

console.log(x.nested !== y.nested);


5
请提供一段代码块的解释,以便其他有类似问题的人能够轻松理解发生了什么。目前,此问题在低质量帖子审核队列中。 - coatless
1
请编辑并添加更多信息。不鼓励仅含代码和“试试这个”的答案,因为它们没有可搜索的内容,也不能解释为什么有人应该“试试这个”。 - Paritosh

6

为了以后参考,当前的 ECMAScript 6 草案引入了 Object.assign 作为克隆对象的一种方法。示例代码如下:

var obj1 = { a: true, b: 1 };
var obj2 = Object.assign(obj1);
console.log(obj2); // { a: true, b: 1 }

目前为止,只有 Firefox 34 浏览器支持 Object.assign,因此它还不能用于生产代码(当然,除非你正在编写 Firefox 扩展程序)。


3
你可能想表达的是 obj2 = Object.assign({}, obj1)。你当前的代码等价于 obj2 = obj1 - Oriol
6
这是一个浅拷贝。 const o1 = { a: { deep: 123 } }; const o2 = Object.assign({}, o1); o2.a.deep = 456; 现在 o1.a.deep === 456 - Josh from Qaribou
3
Object.assign() 不适用于克隆嵌套对象。 - Redu
4
哇,又是一个无用的答案。来自 MDN https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign :关于深度克隆的警告 - 对于深层次的克隆,我们需要使用其他替代方案,因为 Object.assign() 仅会复制属性值。如果源值是对象的引用,则它只会复制该引用值。 - basickarl

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