如何在javascript中深度克隆

How to Deep clone in javascript

如何深入克隆一个javascript对象?

我知道有很多基于框架的函数,比如JSON.parse(JSON.stringify(o))$.extend(true, {}, o),但我不想使用这样的框架。

创建深度克隆的最优雅或最有效的方法是什么?

我们关心的是像克隆阵列这样的边缘情况,而不是破坏原型链,处理自引用。

我们不关心是否支持复制dom对象之类的,因为.cloneNode就是因为这个原因而存在的。

因为我主要想在node.js中使用深度克隆,使用V8引擎的ES5功能是可以接受的。

[编辑]

在任何人提出建议之前,让我先提一下,通过从对象继承原型来创建拷贝和克隆拷贝之间有着明显的区别。前者把原型链搞得一团糟。

[进一步编辑]

在阅读了你的答案之后,我发现克隆整个物体是一个非常危险和困难的游戏。以下面的基于闭包的对象为例

1
2
3
4
5
6
7
8
9
10
11
12
var o = (function() {
     var magic = 42;

     var magicContainer = function() {
          this.get = function() { return magic; };
          this.set = function(i) { magic = i; };
     }

      return new magicContainer;
}());

var n = clone(o); // how to implement clone to support closures

是否有任何方法可以编写克隆对象的克隆函数,克隆时具有相同的状态,但在不使用JS编写JS解析器的情况下无法更改o的状态。

在现实世界中不应该再需要这样的功能了。这只是学术上的兴趣。


非常简单的方法,可能太简单了:

1
var cloned = JSON.parse(JSON.stringify(objectToClone));


这真的取决于你想克隆什么。这是一个真正的JSON对象还是JavaScript中的任何对象?如果你想做任何克隆,它可能会给你带来一些麻烦。哪一个麻烦?我将在下面解释它,但首先是一个代码示例,它克隆对象文本、任何原语、数组和DOM节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
function clone(item) {
    if (!item) { return item; } // null, undefined values check

    var types = [ Number, String, Boolean ],
        result;

    // normalizing primitives if someone did new String('aaa'), or new Number('444');
    types.forEach(function(type) {
        if (item instanceof type) {
            result = type( item );
        }
    });

    if (typeof result =="undefined") {
        if (Object.prototype.toString.call( item ) ==="[object Array]") {
            result = [];
            item.forEach(function(child, index, array) {
                result[index] = clone( child );
            });
        } else if (typeof item =="object") {
            // testing that this is DOM
            if (item.nodeType && typeof item.cloneNode =="function") {
                result = item.cloneNode( true );    
            } else if (!item.prototype) { // check that this is a literal
                if (item instanceof Date) {
                    result = new Date(item);
                } else {
                    // it is an object literal
                    result = {};
                    for (var i in item) {
                        result[i] = clone( item[i] );
                    }
                }
            } else {
                // depending what you would like here,
                // just keep the reference, or create new object
                if (false && item.constructor) {
                    // would not advice to do that, reason? Read below
                    result = new item.constructor();
                } else {
                    result = item;
                }
            }
        } else {
            result = item;
        }
    }

    return result;
}

var copy = clone({
    one : {
        'one-one' : new String("hello"),
        'one-two' : [
           "one","two", true,"four"
        ]
    },
    two : document.createElement("div"),
    three : [
        {
            name :"three-one",
            number : new Number("100"),
            obj : new function() {
                this.name ="Object test";
            }  
        }
    ]
})

现在,我们来谈谈克隆真正的对象时可能会遇到的问题。我现在说的是,你通过做一些类似的事情

1
2
var User = function(){}
var newuser = new User();

当然,您可以克隆它们,这不是问题,每个对象都公开构造函数属性,并且您可以使用它来克隆对象,但它并不总是有效的。您也可以在这个对象上执行简单的for in,但它的方向是相同的—麻烦。我在代码中也包含了克隆功能,但它被if( false )语句排除在外。

那么,为什么克隆会是一种痛苦呢?首先,每个对象/实例都可能有某种状态。你永远不能确定你的对象没有私有变量,如果是这样的话,通过克隆对象,你只会破坏状态。

想象一下没有状态,没关系。那我们还有另一个问题。通过"构造器"方法克隆将给我们带来另一个障碍。这是一个参数依赖关系。你永远无法确定,创造这个物体的人,没有,某种

1
2
3
new User({
   bike : someBikeInstance
});

如果是这样的话,你就不走运了,可能是在某个上下文中创建了某个bikeInstance,而该上下文对于clone方法是未知的。

那么该怎么办呢?您仍然可以执行for in解决方案,并将这些对象视为普通的对象文本,但也许根本不克隆这些对象,只传递此对象的引用是个主意?

另一个解决方案是-您可以设置一个约定,必须克隆的所有对象都应该自己实现这一部分,并提供适当的API方法(如CloneObject)。cloneNode为Dom所做的事情。

你决定。


The JSON.parse(JSON.stringify()) combination to deep copy Javascript objects is an ineffective hack, because JSON does not support values of undefined and function () {}, and therefore JSON.stringify will ignore those sections of code, when"stringifying" (marshalling) the Javascript object into JSON.

以下功能将深度复制对象,不需要第三方库(jquery、lodash等)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function copy(aObject) {
  if (!aObject) {
    return aObject;
  }

  let v;
  let bObject = Array.isArray(aObject) ? [] : {};
  for (const k in aObject) {
    v = aObject[k];
    bObject[k] = (typeof v ==="object") ? copy(v) : v;
  }

  return bObject;
}


下面是一个ES6函数,它也适用于具有循环引用的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function deepClone(obj, hash = new WeakMap()) {
    if (Object(obj) !== obj) return obj; // primitives
    if (obj instanceof Set) return new Set(obj); // See note about this!
    if (hash.has(obj)) return hash.get(obj); // cyclic reference
    const result = obj instanceof Date ? new Date(obj)
                 : obj instanceof RegExp ? new RegExp(obj.source, obj.flags)
                 : obj.constructor ? new obj.constructor()
                 : Object.create(null);
    hash.set(obj, result);
    if (obj instanceof Map)
        Array.from(obj, ([key, val]) => result.set(key, deepClone(val, hash)) );
    return Object.assign(result, ...Object.keys(obj).map (
        key => ({ [key]: deepClone(obj[key], hash) }) ));
}

// Sample data
var p = {
  data: 1,
  children: [{
    data: 2,
    parent: null
  }]
};
p.children[0].parent = p;

var q = deepClone(p);

console.log(q.children[0].parent.data); // 1

关于集合和映射的注释

如何处理集合和地图的键是有争议的:这些键通常是原始的(在这种情况下没有争论),但它们也可以是对象。在这种情况下,问题变成了:应该克隆那些密钥吗?

有人可能会说应该这样做,这样如果那些对象在副本中发生了变异,那么原始对象就不会受到影响,反之亦然。

另一方面,如果一个集合/映射has键,那么在对它们中的任何一个进行任何更改之前,在原始和副本中都应该是这样的。如果副本是一个集合/映射,具有以前从未出现过的键(就像在克隆过程中创建的那样),那将是很奇怪的:对于需要知道给定对象是否是该集合/映射中的键的任何代码来说,这肯定不是很有用。

正如您所注意到的,我更倾向于第二种观点:集合和映射的键是应该保持不变的值(可能是引用)。

这些选择通常也会出现在其他(可能是自定义)对象上。没有通用的解决方案,这在很大程度上取决于克隆对象在特定情况下的行为。


js contrib库库中有一个名为snapshot的函数,可以深度克隆对象。

源代码段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
snapshot: function(obj) {
  if(obj == null || typeof(obj) != 'object') {
    return obj;
  }

  var temp = new obj.constructor();

  for(var key in obj) {
    if (obj.hasOwnProperty(key)) {
      temp[key] = _.snapshot(obj[key]);
    }
  }

  return temp;
}

一旦库链接到您的项目,只需使用

1
_.snapshot(object);


Lo Dash现在是一个underline.js的超集,它有两个深度克隆函数:

  • _.cloneDeep(object)
  • _.cloneDeepWith(object, (val) => {if(_.isElement(val)) return val.cloneNode(true)})

    第二个参数是一个函数,它被调用来生成克隆的值。

从作者本人的回答来看:

lodash underscore build is provided to ensure compatibility with the latest stable version of Underscore.


我认为这是我使用的深度克隆方法太好了,希望你能提出建议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function deepClone (obj) {
    var _out = new obj.constructor;

    var getType = function (n) {
        return Object.prototype.toString.call(n).slice(8, -1);
    }

    for (var _key in obj) {
        if (obj.hasOwnProperty(_key)) {
            _out[_key] = getType(obj[_key]) === 'Object' || getType(obj[_key]) === 'Array' ? deepClone(obj[_key]) : obj[_key];
        }
    }
    return _out;
}

正如其他人在这个和类似的问题上所指出的,在一般意义上,克隆一个"对象"在JavaScript中是可疑的。

但是,有一类对象,我称之为"数据"对象,即那些简单地从{ ... }字面值和/或简单的属性分配构造的对象,或者从json反序列化的对象,对于这些对象,克隆是合理的。就在今天,我想人为地将从服务器接收到的数据膨胀5倍,以测试大型数据集会发生什么情况,但对象(数组)及其子对象必须是不同的对象,才能使其正常工作。克隆允许我这样做,以增加我的数据集:

1
return dta.concat(clone(dta),clone(dta),clone(dta),clone(dta));

另一个我经常结束克隆数据对象的地方是将数据提交回主机,在那里我想在发送数据之前从数据模型中的对象中除去状态字段。例如,在克隆对象时,我可能希望从对象中去掉以"u"开头的所有字段。

这是我最后编写的用于一般性执行此操作的代码,包括支持数组和选择要克隆哪些成员的选择器(使用"path"字符串确定上下文):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function clone(obj,sel) {
    return (obj ? _clone("",obj,sel) : obj);
    }

function _clone(pth,src,sel) {
    var ret=(src instanceof Array ? [] : {});

    for(var key in src) {
        if(!src.hasOwnProperty(key)) { continue; }

        var val=src[key], sub;

        if(sel) {
            sub+=pth+"/"+key;
            if(!sel(sub,key,val)) { continue; }
            }

        if(val && typeof(val)=='object') {
            if     (val instanceof Boolean) { val=Boolean(val);        }
            else if(val instanceof Number ) { val=Number (val);        }
            else if(val instanceof String ) { val=String (val);        }
            else                            { val=_clone(sub,val,sel); }
            }
        ret[key]=val;
        }
    return ret;
    }

最简单合理的深度克隆解决方案是,假设根对象为非空且没有成员选择:

1
2
3
4
5
6
7
8
9
10
function clone(src) {
    var ret=(src instanceof Array ? [] : {});
    for(var key in src) {
        if(!src.hasOwnProperty(key)) { continue; }
        var val=src[key];
        if(val && typeof(val)=='object') { val=clone(val);  }
        ret[key]=val;
        }
    return ret;
    }

我注意到map需要特殊的处理,因此在这个线程中有了所有的建议,代码将是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function deepClone( obj ) {
    if( !obj || true == obj ) //this also handles boolean as true and false
        return obj;
    var objType = typeof( obj );
    if("number" == objType ||"string" == objType ) // add your immutables here
        return obj;
    var result = Array.isArray( obj ) ? [] : !obj.constructor ? {} : new obj.constructor();
    if( obj instanceof Map )
        for( var key of obj.keys() )
            result.set( key, deepClone( obj.get( key ) ) );
    for( var key in obj )
        if( obj.hasOwnProperty( key ) )
            result[key] = deepClone( obj[ key ] );
    return result;
}

使用不变的JS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { fromJS } from 'immutable';

// An object we want to clone
let objA = {
   a: { deep: 'value1', moreDeep: {key: 'value2'} }
};

let immB = fromJS(objA); // Create immutable Map
let objB = immB.toJS(); // Convert to plain JS object

console.log(objA); // Object { a: { deep: 'value1', moreDeep: {key: 'value2'} } }
console.log(objB); // Object { a: { deep: 'value1', moreDeep: {key: 'value2'} } }

// objA and objB are equalent, but now they and their inner objects are undependent
console.log(objA === objB); // false
console.log(objA.a === objB.a); // false
console.log(objA.moreDeep === objB.moreDeep); // false

或住宿/合并

1
2
3
4
5
6
7
8
9
10
11
12
13
import merge from 'lodash/merge'

var objA = {
    a: [{ 'b': 2 }, { 'd': 4 }]
};
// New deeply cloned object:
merge({}, objA );

// We can also create new object from several objects by deep merge:
var objB = {
    a: [{ 'c': 3 }, { 'e': 5 }]
};
merge({}, objA , objB ); // Object { a: [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }

我对所有答案的补充

1
2
3
4
5
6
7
8
deepCopy = arr => {
  if (typeof arr !== 'object') return arr
  if(arr.pop) return [...arr].map(deepCopy)
  const copy = {}
  for (let prop in arr)
    copy[prop] = deepCopy(arr[prop])
  return copy
}


There should be no real world need for such a function anymore. This is mere academic interest.

作为纯粹的练习,这是一种更实用的方法。这是@tfmontague答案的扩展,我建议在这里添加一个防护栏。但鉴于我觉得有必要使用ES6和所有功能,这里是我的皮条客版本。它使逻辑复杂化,因为您必须在数组上映射并在对象上减少,但它避免了任何突变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function cloner(x) {
    const recurseObj = x => typeof x === 'object' ? cloner(x) : x
    const cloneObj = (y, k) => {
        y[k] = recurseObj(x[k])
        return y
    }
    // Guard blocks
    // Add extra for Date / RegExp if you want
    if (!x) {
        return x
    }
    if (Array.isArray(x)) {
        return x.map(recurseObj)
    }
    return Object.keys(x).reduce(cloneObj, {})
}
const tests = [
    null,
    [],
    {},
    [1,2,3],
    [1,2,3, null],
    [1,2,3, null, {}],
    [new Date('2001-01-01')], // FAIL doesn't work with Date
    {x:'', y: {yx: 'zz', yy: null}, z: [1,2,3,null]},
    {
        obj : new function() {
            this.name ="Object test";
        }
    } // FAIL doesn't handle functions
]
tests.map((x,i) => console.log(i, cloner(x)))


我们可以利用递归来制作deepcopy。它可以创建数组、对象、对象数组、具有函数的对象的副本。如果需要,可以为其他类型的数据结构(如map等)添加函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function deepClone(obj) {
         var retObj;
        _assignProps = function(obj, keyIndex, retObj) {
               var subType = Object.prototype.toString.call(obj[keyIndex]);
               if(subType ==="[object Object]" || subType ==="[object Array]") {
                    retObj[keyIndex] = deepClone(obj[keyIndex]);
               }
               else {
                     retObj[keyIndex] = obj[keyIndex];
               }
        };

        if(Object.prototype.toString.call(obj) ==="[object Object]") {
           retObj = {};
           for(key in obj) {
               this._assignProps(obj, key, retObj);
           }
        }
        else if(Object.prototype.toString.call(obj) =="[object Array]") {
           retObj = [];
           for(var i = 0; i< obj.length; i++) {
              this._assignProps(obj, i, retObj);
            }
        };

        return retObj;
    };

这适用于数组、对象和基元。在两种遍历方法之间切换的双递归算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const deepClone = (objOrArray) => {

  const copyArray = (arr) => {
    let arrayResult = [];
    arr.forEach(el => {
        arrayResult.push(cloneObjOrArray(el));
    });
    return arrayResult;
  }

  const copyObj = (obj) => {
    let objResult = {};
    for (key in obj) {
      if (obj.hasOwnProperty(key)) {
        objResult[key] = cloneObjOrArray(obj[key]);
      }
    }
    return objResult;
  }

  const cloneObjOrArray = (el) => {
    if (Array.isArray(el)) {
      return copyArray(el);
    } else if (typeof el === 'object') {
      return copyObj(el);
    } else {
      return el;
    }
  }

  return cloneObjOrArray(objOrArray);
}