在 JavaScript 中深度克隆对象的最有效方法是啥?

Posted

技术标签:

【中文标题】在 JavaScript 中深度克隆对象的最有效方法是啥?【英文标题】:What is the most efficient way to deep clone an object in JavaScript?在 JavaScript 中深度克隆对象的最有效方法是什么? 【发布时间】:2010-09-12 10:25:28 【问题描述】:

克隆 javascript 对象最有效的方法是什么?我见过obj = eval(uneval(o)); 被使用,但that's non-standard and only supported by Firefox。 我做过obj = JSON.parse(JSON.stringify(o)); 之类的事情,但质疑效率。 我还看到了具有各种缺陷的递归复制函数。 我很惊讶没有规范的解决方案存在。

【问题讨论】:

Eval 并不邪恶。使用 eval 不好是。如果你害怕它的副作用,那你就用错了。您担心的副作用是使用它的原因。顺便说一句,有没有人真正回答了你的问题? 克隆对象是一项棘手的工作,尤其是对于任意集合的自定义对象。这可能是为什么没有开箱即用的方法。 eval() 通常是个坏主意,因为many Javascript engine's optimisers have to turn off when dealing with variables that are set via eval。仅在代码中包含 eval() 会导致性能下降。 Most elegant way to clone a JavaScript object的可能重复 请注意,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 【参考方案1】:

按性能进行深度复制: 从最好到最差排名

扩展运算符...(仅原始数组) splice(0)(原始数组 - 仅限) slice()(原始数组 - 仅限) concat()(原始数组 - 仅限) 自定义函数,如下所示(任何数组) jQuery 的$.extend()(任何数组) JSON.parse(JSON.stringify())(原始数组和文字数组 - 仅限) 下划线的_.clone()(仅限原始数组和文字数组) Lo​​dash 的 _.cloneDeep()(任何数组)

地点:

基元 = 字符串、数字和布尔值 文字=对象文字,数组文字[] any = 原语、文字和原型

深拷贝原语数组:

let arr1a = [1, 'a', true];

要深度复制仅包含基元(即数字、字符串和布尔值)的数组,可以使用重新赋值、slice()concat() 和 Underscore 的 clone()

传播速度最快的地方:

let arr1b = [...arr1a];

slice() 的性能比concat() 更好:https://jsbench.me/x5ktn7o94d/

let arr1c = arr1a.splice(0);
let arr1d = arr1a.slice();
let arr1e = arr1a.concat();

深拷贝原始字面量和对象字面量数组:

let arr2a = [1, 'a', true, , []];
let arr2b = JSON.parse(JSON.stringify(arr2a));

深度复制原始数组、对象字面量和原型:

let arr3a = [1, 'a', true, , [], new Object()];

编写自定义函数(性能比$.extend()JSON.parse 更快):

function copy(o) 
  let out, v, key;
  out = Array.isArray(o) ? [] : ;
  for (key in o) 
    v = o[key];
    out[key] = (typeof v === "object" && v !== null) ? copy(v) : v;
  
  return out;


let arr3b = copy(arr3a);

或使用第三方实用功能:

let arr3c = $.extend(true, [], arr3a); // jQuery Extend
let arr3d = _.cloneDeep(arr3a); // Lodash

注意:jQuery 的$.extend 也比JSON.parse(JSON.stringify()) 有更好的性能:

js-deep-copy jquery-extend-vs-json-parse

【讨论】:

使用 for-in 循环,您应该使用 hasOwnProperty 排除继承的属性。我在Object.keys 上使用(可能更快)纯 for 循环。 在深拷贝中,您不想复制继承的属性吗?另外,请注意,调用 hasOwnProperty 方法会为每个键创建性能损失(将函数调用推入和推下堆栈,并执行方法代码)。【参考方案2】:

使用当今的 JavaScript 克隆对象:ECMAScript 2015(以前称为 ECMAScript 6)

var original = a: 1;

// Method 1: New object with original assigned.
var copy1 = Object.assign(, original);

// Method 2: New object with spread operator assignment.
var copy2 = ...original;

旧版浏览器可能不支持 ECMAScript 2015。一种常见的解决方案是使用 JavaScript 到 JavaScript 的编译器(如 Babel)输出 JavaScript 代码的 ECMAScript 5 版本。

作为pointed out by @jim-hall,这只是一个浅拷贝。属性的属性被复制作为参考:更改一个会更改另一个对象/实例中的值。

【讨论】:

这不涉及深度合并。 gist.github.com/jimbol/5d5a3e3875c34abcf60a 哇,这个答案太错误了。您的两种方法都执行一个级别的浅拷贝。任何看到这个答案的人,继续前进。【参考方案3】:

由于递归对于 JavaScript 来说太昂贵了,而且我发现的大多数答案都是使用递归,而 JSON 方法将跳过不可转换为 JSON 的部分(函数等)。所以我做了一些研究,发现这种蹦床技术可以避免它。代码如下:

/*
 * 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;
;

【讨论】:

尾调用递归实际上在大多数 JavaScript 实现中都非常高效,需要在 ES6 中进行优化。 你好,当时我做了一个小测试,当目标对象变得复杂时,调用堆栈很容易溢出,虽然我没有做任何笔记,希望在es6中这将是一个大操作. 堆栈很容易溢出,可能是因为循环引用。【参考方案4】:

仅当您可以使用ECMAScript 6 或transpilers 时。

特点:

复制时不会触发 getter/setter。 保留 getter/setter。 保留原型信息。 适用于 object-literalfunctional OO 写作风格。

代码:

function clone(target, source)

    for(let key in source)

        // Use getOwnPropertyDescriptor instead of source[key] to prevent from trigering setter/getter.
        let descriptor = Object.getOwnPropertyDescriptor(source, key);
        if(descriptor.value instanceof String)
            target[key] = new String(descriptor.value);
        
        else if(descriptor.value instanceof Array)
            target[key] = clone([], descriptor.value);
        
        else if(descriptor.value instanceof Object)
            let prototype = Reflect.getPrototypeOf(descriptor.value);
            let cloneObject = clone(, descriptor.value);
            Reflect.setPrototypeOf(cloneObject, prototype);
            target[key] = cloneObject;
        
        else 
            Object.defineProperty(target, key, descriptor);
        
    
    let prototype = Reflect.getPrototypeOf(source);
    Reflect.setPrototypeOf(target, prototype);
    return target;

【讨论】:

Date等数据类型存在问题 如果与具有null 原型的对象一起使用,这将创建对同一对象实例的引用( 深复制它),因为Object.create(null) instanceof Object 为假.【参考方案5】:

浅拷贝单行(ECMAScript 5th edition):

var origin =  foo :  ;
var copy = Object.keys(origin).reduce(function(c,k)c[k]=origin[k];return c;,);

console.log(origin, copy);
console.log(origin == copy); // false
console.log(origin.foo == copy.foo); // true

浅拷贝单行(ECMAScript 6th edition, 2015):

var origin =  foo :  ;
var copy = Object.assign(, origin);

console.log(origin, copy);
console.log(origin == copy); // false
console.log(origin.foo == copy.foo); // true

【讨论】:

【参考方案6】:

如果您正在使用它,Underscore.js 库有一个 clone 方法。

var newObject = _.clone(oldObject);

【讨论】:

这是一个浅拷贝,而不是像 OP 正在寻找的深拷贝。【参考方案7】:

在 JS 中克隆一个对象一直是一个问题,但这都是在 ES6 之前的事情,我在下面列出了在 JavaScript 中复制对象的不同方法,假设你有下面的 Object 并且想要对其进行深层复制:

var obj = a:1, b:2, c:3, d:4;

有几种方法可以在不改变原点的情况下复制这个对象:

    ES5+,用一个简单的函数为你做拷贝:

    function deepCopyObj(obj) 
        if (null == obj || "object" != typeof obj) return obj;
        if (obj instanceof Date) 
            var copy = new Date();
            copy.setTime(obj.getTime());
            return copy;
        
        if (obj instanceof Array) 
            var copy = [];
            for (var i = 0, len = obj.length; i < len; i++) 
                copy[i] = deepCopyObj(obj[i]);
            
            return copy;
        
        if (obj instanceof Object) 
            var copy = ;
            for (var attr in obj) 
                if (obj.hasOwnProperty(attr)) copy[attr] = deepCopyObj(obj[attr]);
            
            return copy;
        
        throw new Error("Unable to copy obj this object.");
    
    

    ES5+,使用JSON.parseJSON.stringify

    var deepCopyObj = JSON.parse(JSON.stringify(obj));
    

    角度:

    var deepCopyObj = angular.copy(obj);
    

    jQuery:

    var deepCopyObj = jQuery.extend(true, , obj);
    

    Underscore.js 和 Lodash:

    var deepCopyObj = _.cloneDeep(obj); //latest version of Underscore.js makes shallow copy
    

希望这些帮助……

【讨论】:

【参考方案8】:

结构化克隆

2021 年更新:structuredClone global function 即将登陆浏览器、Node.js 和 Deno。

html 标准包括an internal structured cloning/serialization algorithm,可以创建对象的深层克隆。它仍然仅限于某些内置类型,但除了 JSON 支持的少数类型之外,它还支持 Dates、RegExps、Maps、Sets、Blobs、FileLists、ImageDatas、稀疏数组、类型化数组,未来可能还会更多.它还保留克隆数据中的引用,允许它支持会导致 JSON 错误的循环和递归结构。

Node.js 中的支持:实验性?

structuredClone global function 很快将由 Node.js 提供:

const clone = structuredClone(original);

在那之前:Node.js 中的 v8 模块目前(截至 Node 11)exposes the structured serialization API directly,但此功能仍被标记为“实验性”,并且在未来版本中可能会更改或删除。如果您使用的是兼容版本,则克隆对象非常简单:

const v8 = require('v8');

const structuredClone = obj => 
  return v8.deserialize(v8.serialize(obj));
;

浏览器中的直接支持:即将推出?

structuredClone global function 很快将由所有主要浏览器提供(之前已在whatwg/html#793 on GitHub 中讨论过)。它看起来/将看起来像这样:

const clone = structuredClone(original);

在发布之前,浏览器的结构化克隆实现只是间接公开。

异步解决方法:可用。 ?

使用现有 API 创建结构化克隆的开销较低的方法是通过 MessageChannels 的一个端口发布数据。另一个端口将发出message 事件,其中包含附加的.data 的结构化克隆。不幸的是,监听这些事件必然是异步的,同步的替代方案不太实用。

class StructuredCloner 
  constructor() 
    this.pendingClones_ = new Map();
    this.nextKey_ = 0;
    
    const channel = new MessageChannel();
    this.inPort_ = channel.port1;
    this.outPort_ = channel.port2;
    
    this.outPort_.onmessage = (data: key, value) => 
      const resolve = this.pendingClones_.get(key);
      resolve(value);
      this.pendingClones_.delete(key);
    ;
    this.outPort_.start();
  

  cloneAsync(value) 
    return new Promise(resolve => 
      const key = this.nextKey_++;
      this.pendingClones_.set(key, resolve);
      this.inPort_.postMessage(key, value);
    );
  


const structuredCloneAsync = window.structuredCloneAsync =
    StructuredCloner.prototype.cloneAsync.bind(new StructuredCloner);

使用示例:

const main = async () => 
  const original =  date: new Date(), number: Math.random() ;
  original.self = original;

  const clone = await structuredCloneAsync(original);

  // They're different objects:
  console.assert(original !== clone);
  console.assert(original.date !== clone.date);

  // They're cyclical:
  console.assert(original.self === original);
  console.assert(clone.self === clone);

  // They contain equivalent values:
  console.assert(original.number === clone.number);
  console.assert(Number(original.date) === Number(clone.date));
  
  console.log("Assertions complete.");
;

main();

同步解决方法:太糟糕了! ?

同步创建结构化克隆没有很好的选择。这里有一些不切实际的技巧。

history.pushState()history.replaceState() 都创建了第一个参数的结构化克隆,并将该值分配给history.state。您可以使用它来创建任何对象的结构化克隆,如下所示:

const structuredClone = obj => 
  const oldState = history.state;
  history.replaceState(obj, null);
  const clonedObj = history.state;
  history.replaceState(oldState, null);
  return clonedObj;
;

使用示例:

'use strict';

const main = () => 
  const original =  date: new Date(), number: Math.random() ;
  original.self = original;

  const clone = structuredClone(original);
  
  // They're different objects:
  console.assert(original !== clone);
  console.assert(original.date !== clone.date);

  // They're cyclical:
  console.assert(original.self === original);
  console.assert(clone.self === clone);

  // They contain equivalent values:
  console.assert(original.number === clone.number);
  console.assert(Number(original.date) === Number(clone.date));
  
  console.log("Assertions complete.");
;

const structuredClone = obj => 
  const oldState = history.state;
  history.replaceState(obj, null);
  const clonedObj = history.state;
  history.replaceState(oldState, null);
  return clonedObj;
;

main();

虽然是同步的,但这可能非常慢。它会产生与操作浏览器历史相关的所有开销。重复调用此方法可能会导致 Chrome 暂时无响应。

Notification constructor 创建其关联数据的结构化克隆。它还尝试向用户显示浏览器通知,但除非您请求通知权限,否则这将静默失败。如果您有其他用途的许可,我们将立即关闭我们创建的通知。

const structuredClone = obj => 
  const n = new Notification('', data: obj, silent: true);
  n.onshow = n.close.bind(n);
  return n.data;
;

使用示例:

'use strict';

const main = () => 
  const original =  date: new Date(), number: Math.random() ;
  original.self = original;

  const clone = structuredClone(original);
  
  // They're different objects:
  console.assert(original !== clone);
  console.assert(original.date !== clone.date);

  // They're cyclical:
  console.assert(original.self === original);
  console.assert(clone.self === clone);

  // They contain equivalent values:
  console.assert(original.number === clone.number);
  console.assert(Number(original.date) === Number(clone.date));
  
  console.log("Assertions complete.");
;

const structuredClone = obj => 
  const n = new Notification('', data: obj, silent: true);
  n.close();
  return n.data;
;

main();

【讨论】:

这是大错特错!该 API 不适合以这种方式使用。 作为在 Firefox 中实现 pushState 的人,我对这次 hack 感到自豪和厌恶的奇怪混合。干得好,伙计们。 pushState 或 Notification hack 不适用于 Function 等某些对象类型 @ShishirArora 你说得对,我刚试了一下,它会抛出“未捕获的 DOMException:无法克隆对象。”通知黑客也是如此。 有谁知道原生 v8 结构化克隆是否容易受到原型污染?【参考方案9】:

原生深度克隆

它被称为“结构化克隆”,在 Node 11 及更高版本中实验性地工作,并有望在浏览器中落地。详情请见this answer。

带数据丢失的快速克隆 - JSON.parse/stringify

如果您在对象中不使用Dates、函数、undefinedInfinity、RegExps、Maps、Sets、Blobs、FileLists、ImageDatas、稀疏数组、类型化数组或其他复杂类型,则非常深度克隆对象的简单方法是:

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'
  re: /.*/,  // lost

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()

有关基准,请参阅 Corban's answer。

使用库进行可靠克隆

由于克隆对象并非易事(复杂类型、循环引用、函数等),大多数主要库都提供了克隆对象的函数。 不要重新发明*** - 如果您已经在使用库,请检查它是否具有对象克隆功能。例如,

lodash - cloneDeep;可以通过 lodash.clonedeep 模块单独导入,如果您尚未使用提供深度克隆功能的库,这可能是您的最佳选择 AngularJS - angular.copy jQuery - jQuery.extend(true, , oldObject); .clone() 只克隆 DOM 元素 只是图书馆-just-clone;零依赖 npm 模块库的一部分,它只做一件事。 适合各种场合的无罪实用程序。

ES6(复制)

为了完整起见,请注意 ES6 提供了两种浅拷贝机制:Object.assign() 和 spread syntax。 它将所有可枚举自身属性的值从一个对象复制到另一个对象。例如:

var A1 = a: "2";
var A2 = Object.assign(, A1);
var A3 = ...A1;  // Spread Syntax

【讨论】:

当心! var A = b: [ a: [ 1, 2, 3], b: [4, 5, 6], c: [7, 8, 9] ] ; B = Object.assign( , A ); delete B.b[0].b; 它也会修改对象 A ! @Gabriel Hautclocq 这是因为A.bB.b 都指向内存中的同一个对象。如果A 的属性具有非对象值(如数字或字符串),它将被正常复制。但是,当复制包含对象值的属性时,它是按引用复制的,而不是按值复制的。另外,请记住,数组是 JS 中的对象。证明:typeof [] == 'object' &amp;&amp; [] instanceof Array @Unicornist 是的,这就是为什么 Object.assign 没有回答以下问题:“在 JavaScript 中深度克隆对象的最有效方法是什么?”。所以至少它不应该作为 ES6 的深度克隆解决方案出现。标题“ES6”具有误导性,至少应该更改以反映这不是深度克隆方法。 “浅”这个词很容易被忽略,很多人只是采用他们在 Stack Overflow 中找到的最简单的解决方案,而没有阅读所有内容。依赖 Object.assign 进行对象克隆是危险的。因此我的评论。 我使用了一个名为真正快速深度克隆的库:github.com/davidmarkclements/rfdc 非常适合我。 @Ricardo 当然,在我写下我的评论之后,你可以看到答案的历史,看到“ES6”之后添加了“(浅拷贝)”。现在更清楚这是一个浅拷贝。【参考方案10】:

我回答这个问题迟了,但我有另一种克隆对象的方法:

function cloneObject(obj) 
    if (obj === null || typeof(obj) !== 'object')
        return obj;
    var temp = obj.constructor(); // changed
    for (var key in obj) 
        if (Object.prototype.hasOwnProperty.call(obj, key)) 
            obj['isActiveClone'] = null;
            temp[key] = cloneObject(obj[key]);
            delete obj['isActiveClone'];
        
    
    return temp;


var b = cloneObject("a":1,"b":2);   // calling

这样更好更快:

var a = "a":1,"b":2;
var b = JSON.parse(JSON.stringify(a));  

var a = "a":1,"b":2;

// Deep copy
var newObject = jQuery.extend(true, , a);

我已经对代码进行了基准测试,您可以测试结果here:

并分享结果: 参考文献:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty

【讨论】:

这很有趣,但是当我运行你的测试时,它实际上让我觉得方法 1 是最慢的一种 和我一样,block 1最低! 唯一对我有用的解决方案!必须深度克隆一个对象,该对象包含具有函数属性的其他对象。完美。 为什么设置obj['isActiveClone'] = null然后删除呢?你为什么不打电话给obj.hasOwnProperty(key)【参考方案11】:

对于想要使用JSON.parse(JSON.stringify(obj)) 版本但又不会丢失Date 对象的人,您可以使用second argument of parse method 将字符串转换回Date:

function clone(obj) 
  var regExp = /^\d4-\d2-\d2T\d2:\d2:\d2\.\d3Z$/;
  return JSON.parse(JSON.stringify(obj), function(k, v) 
    if (typeof v === 'string' && regExp.test(v))
      return new Date(v)
    return v;
  )


// usage:
var original = 
 a: [1, null, undefined, 0, a:null, new Date()],
 b: 
   c() return 0 
 


var cloned = clone(original)

console.log(cloned)

【讨论】:

不是 100% 的克隆【参考方案12】:

在 JavaScript 中深度复制对象(我认为最好也最简单)

1.使用 JSON.parse(JSON.stringify(object));

var obj =  
  a: 1,
  b:  
    c: 2
  

var newObj = JSON.parse(JSON.stringify(obj));
obj.b.c = 20;
console.log(obj); //  a: 1, b:  c: 20  
console.log(newObj); //  a: 1, b:  c: 2   

2.使用创建的方法

function cloneObject(obj) 
    var clone = ;
    for(var i in obj) 
        if(obj[i] != null &&  typeof(obj[i])=="object")
            clone[i] = cloneObject(obj[i]);
        else
            clone[i] = obj[i];
    
    return clone;


var obj =  
  a: 1,
  b:  
    c: 2
  

var newObj = cloneObject(obj);
obj.b.c = 20;

console.log(obj); //  a: 1, b:  c: 20  
console.log(newObj); //  a: 1, b:  c: 2   

3.使用 Lo-Dash 的 _.cloneDeep 链接lodash

var obj =  
  a: 1,
  b:  
    c: 2
  


var newObj = _.cloneDeep(obj);
obj.b.c = 20;
console.log(obj); //  a: 1, b:  c: 20  
console.log(newObj); //  a: 1, b:  c: 2   

4.使用 Object.assign() 方法

var obj =  
  a: 1,
  b: 2


var newObj = _.clone(obj);
obj.b = 20;
console.log(obj); //  a: 1, b: 20 
console.log(newObj); //  a: 1, b: 2   

但错误的时候

var obj =  
  a: 1,
  b:  
    c: 2
  


var newObj = Object.assign(, obj);
obj.b.c = 20;
console.log(obj); //  a: 1, b:  c: 20  
console.log(newObj); //  a: 1, b:  c: 20   --> WRONG
// Note: Properties on the prototype chain and non-enumerable properties cannot be copied.

5.使用Underscore.js _.clone链接Underscore.js

var obj =  
  a: 1,
  b: 2


var newObj = _.clone(obj);
obj.b = 20;
console.log(obj); //  a: 1, b: 20 
console.log(newObj); //  a: 1, b: 2   

但错误的时候

var obj =  
  a: 1,
  b:  
    c: 2
  


var newObj = _.cloneDeep(obj);
obj.b.c = 20;
console.log(obj); //  a: 1, b:  c: 20  
console.log(newObj); //  a: 1, b:  c: 20   --> WRONG
// (Create a shallow-copied clone of the provided plain object. Any nested objects or arrays will be copied by reference, not duplicated.)

JSBEN.CH Performance Benchmarking Playground 1~3 http://jsben.ch/KVQLd

【讨论】:

嘿,你的最后一个例子是错误的。在我看来,你必须使用 _clone 而不是 _cloneDeep 作为错误的例子。 这个创建的方法 (2.) 不适用于数组,不是吗? 方法 #2 容易受到原型污染,类似于 lodash 的 defaultsDeep 所发生的情况。如果(i === '__proto__')不应该复制,如果(i === 'constuctor' &amp;&amp; typeof obj[i] === 'function')不应该复制。【参考方案13】:

查看此基准:http://jsben.ch/#/bWfk9

在我之前的测试中,我发现速度是一个主要问题

JSON.parse(JSON.stringify(obj))

成为深度克隆对象的最慢方式(它比 jQuery.extend 慢,deep 标志设置为 true 10-20%)。

deep 标志设置为false(浅克隆)时,jQuery.extend 非常快。这是一个不错的选择,因为它包含一些用于类型验证的额外逻辑,并且不会复制未定义的属性等,但这也会让你慢一点。

如果你知道你试图克隆的对象的结构或者可以避免深度嵌套数组,你可以编写一个简单的for (var i in obj) 循环来克隆你的对象,同时检查 hasOwnProperty,它会比 jQuery 快得多。

最后,如果您尝试在热循环中克隆一个已知的对象结构,您可以通过简单地内联克隆过程并手动构造对象来获得更多的性能。

JavaScript 跟踪引擎在优化 for..in 循环方面很糟糕,检查 hasOwnProperty 也会减慢您的速度。当速度是绝对必须时手动克隆。

var clonedObject = 
  knownProp: obj.knownProp,
  ..

注意在Date 对象上使用JSON.parse(JSON.stringify(obj)) 方法 - JSON.stringify(new Date()) 以 ISO 格式返回日期的字符串表示形式,JSON.parse() 不会 转换回 @987654336 @ 目的。 See this answer for more details.

另外,请注意,至少在 Chrome 65 中,本机克隆不是要走的路。根据 JSPerf 的说法,通过创建一个新函数执行本机克隆比使用 JSON.stringify 慢近 800 倍,后者的速度非常快。

Update for ES6

如果您使用的是 Javascript ES6,请尝试使用本机方法进行克隆或浅拷贝。

Object.assign(, obj);

【讨论】:

请注意,您的工作台有两个错误:首先,它将一些浅克隆(lodash _.cloneObject.assign)与一些深度克隆(JSON.parse(JSON.stringify()))进行了比较。其次,它对 lodash 说的是“深度克隆”,但实际上是浅层克隆。【参考方案14】:

这是我的解决方案,不使用任何库或原生 javascript 函数。

function deepClone(obj) 
  if (typeof obj !== "object") 
    return obj;
   else 
    let newObj =
      typeof obj === "object" && obj.length !== undefined ? [] : ;
    for (let key in obj) 
      if (key) 
        newObj[key] = deepClone(obj[key]);
      
    
    return newObj;
  

【讨论】:

小心... const o = ; o.a = o; deepClone(o); -> 递归错误。【参考方案15】:

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

【讨论】:

远非“最佳”。也许对于简单的对象。【参考方案16】:

我的情况有点不同。我有一个带有嵌套对象和函数的对象。因此,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);

对于与效率相关的问题,我相信这是解决我遇到的问题的最简单和最有效的解决方案。我会很感激这个算法上的任何 cmets 可以使它更有效。

【讨论】:

【参考方案17】:

随着新方法Object.fromEntries() 的提议,某些浏览器的较新版本(reference)支持该方法。我想为下一个递归方法做出贡献:

const obj = 
  key1: key11: "key11", key12: "key12", key13: key131: 22,
  key2: key21: "key21", key22: "key22",
  key3: "key3",
  key4: [1,2,3, key: "value"]


const cloneObj = (obj) =>

    if (Object(obj) !== obj)
       return obj;
    else if (Array.isArray(obj))
       return obj.map(cloneObj);

    return Object.fromEntries(Object.entries(obj).map(
        ([k,v]) => ([k, cloneObj(v)])
    ));


// Clone the original object.
let newObj = cloneObj(obj);

// Make changes on the original object.
obj.key1.key11 = "TEST";
obj.key3 = "TEST";
obj.key1.key13.key131 = "TEST";
obj.key4[1] = "TEST";
obj.key4[3].key = "TEST";

// Display both objects on the console.
console.log("Original object: ", obj);
console.log("Cloned object: ", newObj);
.as-console background-color:black !important; color:lime;
.as-console-wrapper max-height:100% !important; top:0;

【讨论】:

【参考方案18】:

如何将对象的与其合并?

function deepClone(o) 
    var keys = Object.keys(o);
    var values = Object.values(o);

    var clone = ;

    keys.forEach(function(key, i) 
        clone[key] = typeof values[i] == 'object' ? Object.create(values[i]) : values[i];
    );

    return clone;

注意: 这种方法不一定会进行浅拷贝,但它只复制一个内部对象的深度,这意味着当你得到类似的东西时a: b: c: null,它只会克隆直接在其中的对象,所以deepClone(a.b).c 在技术上是对a.b.c 的引用,而deepClone(a).b 是一个克隆,不是引用

【讨论】:

【参考方案19】:
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.");

使用以下方法代替JSON.parse(JSON.stringify(obj)),因为 它比下面的方法慢

How do I correctly clone a JavaScript object?

【讨论】:

【参考方案20】:

在 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;

【讨论】:

这很容易受到全局对象污染。如果(prop === 'constuctor' &amp;&amp; typeof src[prop] === 'function')(prop === '__proto__'),则不应复制prop【参考方案21】:

如果您发现自己经常做这种事情(例如创建撤消重做功能),那么可能值得研究一下Immutable.js

const map1 = Immutable.fromJS(  a: 1, b: 2, c:  d: 3   );
const map2 = map1.setIn( [ 'c', 'd' ], 50 );

console.log( `$ map1.getIn( [ 'c', 'd' ] )  vs $ map2.getIn( [ 'c', 'd' ] ) ` ); // "3 vs 50"

https://codepen.io/anon/pen/OBpqNE?editors=1111

【讨论】:

【参考方案22】:

如果没有内置的,你可以试试:

function clone(obj) 
    if (obj === null || typeof (obj) !== 'object' || 'isActiveClone' in obj)
        return obj;

    if (obj instanceof Date)
        var temp = new obj.constructor(); //or new Date(obj);
    else
        var temp = obj.constructor();

    for (var key in obj) 
        if (Object.prototype.hasOwnProperty.call(obj, key)) 
            obj['isActiveClone'] = null;
            temp[key] = clone(obj[key]);
            delete obj['isActiveClone'];
        
    
    return temp;

【讨论】:

【参考方案23】:

当您的对象嵌套并包含数据对象、其他结构化对象或某些属性对象等时,使用JSON.parse(JSON.stringify(object))Object.assign(, obj)$.extend(true, , obj) 将不起作用。在这种情况下,请使用 lodash。简单易行..

var obj = a: 25, b: a: 1, b: 2, c: new Date(), d: anotherNestedObject ;
var A = _.cloneDeep(obj);

现在 A 将是你的 obj 的新克隆,没有任何引用..

【讨论】:

【参考方案24】:

希望这会有所帮助。

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

【讨论】:

【参考方案25】:

对于浅拷贝,ECMAScript2018 标准中引入了一种很棒的简单方法。它涉及到 Spread Operator 的使用:

let obj = a : "foo", b:"bar" , c:10 , d:true , e:[1,2,3] ;

let objClone =  ...obj ;

我已经在 Chrome 浏览器中对其进行了测试,两个对象都存储在不同的位置,因此在其中一个中更改直接子值不会改变另一个。虽然(在示例中)更改 e 中的值会影响两个副本。

这种技术非常简单直接。我认为这是一劳永逸地解决这个问题的真正最佳实践。

【讨论】:

更新 objClone 中的 e 仍会更新 obj 中的 e。这仍然只是一个浅拷贝。该问题明确要求进行深度克隆。 @Taugenichts...你测试了吗?该方法完美运行。 Spread_syntaxSpread in object literals部分 是的,我测试过了。运行此代码:objClone.e[4] = 5;控制台日志(obj.e);您将看到 obj.e 正在更新 因为两者都存储在不同的位置仅仅意味着它至少是一个浅拷贝。查看obj.eobjClone.e的存储位置;你会发现它们存储在同一个位置。 非常感谢你们@LupusOssorum @Taugenichts 指出这一点。我自己测试了一下,发现你们在这里发现了什么。但是你知道为什么数组仍然不改变内存,尽管 ECMA2018 吹嘘这是一个特性。【参考方案26】:

根据我的经验,递归版本的性能大大优于 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]
  ), );

这是在40 times faster 周围执行而不是JSON.parse... 方法。

【讨论】:

伪代码将是:对于每个键,将其值分配给新对象中的相同键(浅拷贝)。但是,如果该值的类型为Object(不能浅拷贝),则该函数会以该值作为参数递归调用自身。 太糟糕了,当值是一个数组时它不能正常工作。但是,不应该太难修改以使其适用于这种情况。 TypeError: 无法读取未定义的属性“构造函数”【参考方案27】:

ES 2017 示例:

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

【讨论】:

感谢您的回答。我尝试了您的方法,但不幸的是,它不起作用。因为这可能是我这边的某种错误,所以我请你检查我的example in JSFiddle,如果我这边有错误,我会投票支持你的答案。 当我运行你的小提琴时,我得到 foo: 1, bar: fooBar: 22, fooBaz: 33, fooFoo: 11 , baz: 3 foo: 1, bar: fooBar: 22, fooBaz: 44, fooFoo: 11 , baz: 4。这不是您所期望的吗? 您粘贴的是我所期望的。我不明白为什么,但我在控制台中看到testObj2testObj3fooBaz: 44... (screenshot) 这不是深拷贝,而是浅拷贝。 @GurebuBokofu【参考方案28】:

浏览这个长长的答案列表,几乎所有的解决方案都被涵盖了,除了我知道的一个。这是 VANILLA JS 深度克隆对象的方法列表。

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

    通过带有 pushState 或 replaceState 的 history.state

    Web 通知 API,但这样做的缺点是要求用户授予权限。

    通过对象执行自己的递归循环以复制每个级别。

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

【讨论】:

所有这些都已经在答案或 cmets 中进行了转换。不过,如果您为每个代码示例提供唯一的代码示例,我会投票赞成。【参考方案29】:

Promise 完成的异步对象克隆怎么样?

async function clone(thingy /**/)

    if(thingy instanceof Promise)
    
        throw Error("This function cannot clone Promises.");
    
    return thingy;

【讨论】:

等一下,5 个支持者,它是如何运作的?我自己忘记了,这看起来违反直觉,现在已经过去了一年半。 不知道它应该做什么,我很困惑:s Promise.resolve(value) 是否解析克隆的value?我怀疑它,超越我自己。【参考方案30】:
// obj target object, vals source object
var setVals = function (obj, vals) 
    if (obj && vals) 
        for (var x in vals) 
            if (vals.hasOwnProperty(x)) 
                if (obj[x] && typeof vals[x] === 'object') 
                    obj[x] = setVals(obj[x], vals[x]);
                 else 
                    obj[x] = vals[x];
                
            
        
    
    return obj;
;

【讨论】:

以上是关于在 JavaScript 中深度克隆对象的最有效方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

在 JavaScript 中深度克隆对象的最有效方法是啥?

在 JavaScript 中深度克隆对象的最有效方法是啥?

Stackoverflow热门问题

具有属性子集的对象数组克隆

在 Ruby 中深度复制对象的最有效方法是啥?

26.JavaScript实现对象混合与对象浅度克隆和对象的深度克隆