如何正确克隆 JavaScript 对象?

问:

我有一个对象 x。我想将它复制为对象 y,这样对 y 的更改就不会修改 x。我意识到复制从内置 JavaScript 对象派生的对象会导致额外的、不需要的属性。这不是问题,因为我正在复制我自己的文字构造对象之一。

如何正确克隆 JavaScript 对象?

答1:

huntsbot.com聚合了超过10+全球外包任务平台的外包需求,寻找外包任务与机会变的简单与高效。

2022 年更新

有一个名为 structured cloning 的新 JS 标准。它适用于所有浏览器:

const clone = structuredClone(object);

旧答案

对 JavaScript 中的任何对象执行此操作都不会简单或直接。您将遇到错误地从对象原型中获取属性的问题,这些属性应该留在原型中而不是复制到新实例中。例如,如果您将 clone 方法添加到 Object.prototype,正如一些答案所描述的,您将需要明确跳过该属性。但是,如果在 Object.prototype 中添加了其他附加方法,或者其他中间原型,您不知道怎么办?在这种情况下,您将复制不应复制的属性,因此您需要使用 hasOwnProperty 方法检测不可预见的非本地属性。

除了不可枚举的属性之外,当您尝试复制具有隐藏属性的对象时,您还会遇到更棘手的问题。例如,prototype 是函数的隐藏属性。此外,对象的原型由属性 proto 引用,该属性也是隐藏的,不会被迭代源对象属性的 for/in 循环复制。我认为 proto 可能特定于 Firefox 的 JavaScript 解释器,并且在其他浏览器中可能有所不同,但你明白了。并非所有事物都是可枚举的。如果您知道它的名称,您可以复制隐藏的属性,但我不知道有什么方法可以自动发现它。

寻求优雅解决方案的另一个障碍是正确设置原型继承的问题。如果您的源对象的原型是 Object,那么只需使用 {} 创建一个新的通用对象即可,但如果源的原型是 Object 的某个后代,那么您将丢失该原型的其他成员您使用 hasOwnProperty 过滤器跳过了哪些,或者哪些在原型中,但一开始就无法枚举。一种解决方案可能是调用源对象的 constructor 属性来获取初始复制对象,然后复制属性,但是您仍然不会获得不可枚举的属性。例如,Date 对象将其数据存储为隐藏成员:

function clone(obj) {
    if (null == obj || "object" != typeof obj) return obj;
    var copy = obj.constructor();
    for (var attr in obj) {
        if (obj.hasOwnProperty(attr)) copy[attr] = obj[attr];
    }
    return copy;
}

var d1 = new Date();

/* Executes function after 5 seconds. */
setTimeout(function(){
    var d2 = clone(d1);
    alert("d1 = " + d1.toString() + "\nd2 = " + d2.toString());
}, 5000);

d1 的日期字符串将比 d2 晚 5 秒。使一个 Date 与另一个相同的方法是调用 setTime 方法,但这是特定于 Date 类的。我认为这个问题没有万无一失的通用解决方案,尽管我很乐意犯错!

当我不得不实现一般的深度复制时,我最终妥协,假设我只需要复制一个普通的 Object、Array、Date、String、Number 或 Boolean。最后 3 种类型是不可变的,所以我可以执行浅拷贝而不用担心它会改变。我进一步假设 Object 或 Array 中包含的任何元素也将是该列表中的 6 个简单类型之一。这可以通过如下代码来完成:

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.");
}

只要对象和数组中的数据形成树形结构,上述函数就可以充分适用于我提到的 6 种简单类型。也就是说,对象中对相同数据的引用不超过一个。例如:

// This would be cloneable:
var tree = {
    "left"  : { "left" : null, "right" : null, "data" : 3 },
    "right" : null,
    "data"  : 8
};

// This would kind-of work, but you would get 2 copies of the 
// inner node instead of 2 references to the same copy
var directedAcylicGraph = {
    "left"  : { "left" : null, "right" : null, "data" : 3 },
    "data"  : 8
};
directedAcyclicGraph["right"] = directedAcyclicGraph["left"];

// Cloning this would cause a stack overflow due to infinite recursion:
var cyclicGraph = {
    "left"  : { "left" : null, "right" : null, "data" : 3 },
    "data"  : 8
};
cyclicGraph["right"] = cyclicGraph;

它不能处理任何 JavaScript 对象,但它可能足以满足许多目的,只要你不认为它只适用于你扔给它的任何东西。

一个优秀的自由职业者,应该有对需求敏感和精准需求捕获的能力,而huntsbot.com提供了这个机会

这是缺少符号键和符号值。如今,使用 Object.getOwnPropertyDescriptors 更好。

structuredClone 是 only 75% compatible globally

在 Nodejs 中,structuredClone(object) 适用于节点 v17.0.0 及更高版本。

@JoshuaDavid 的更新,目前 82.57% 的浏览器都支持。

答2:

huntsbot.com高效搞钱,一站式跟进超10+任务平台外包需求

如果您不在对象中使用 Date、函数、未定义、正则表达式或 Infinity,则一个非常简单的一行是 JSON.parse(JSON.stringify(object)):

const a = { string: ‘string’, number: 123, bool: false, nul: null, date: new Date(), // 字符串化 undef: undefined, // 丢失 inf: Infinity, // 强制为 ‘null’ } 控制台.log(a); console.log(typeof a.date); // 日期对象 const clone = JSON.parse(JSON.stringify(a));控制台.log(克隆); console.log(typeof clone.date); // .toISOString() 的结果

这适用于包含对象、数组、字符串、布尔值和数字的所有类型的对象。

另请参阅在向工作人员发送消息或从工作人员发送消息时使用的 this article about the structured clone algorithm of browsers。它还包含深度克隆功能。

有时最好的答案是最简单的。天才。

有帮助,但是在比较包含其他对象的对象时,当两个完全相等的对象不被视为相等时,我遇到了意外的行为。使用 JSON.stringify(x) == JSON.stringify(JSON.parse(JSON.stringify(a))) 来修复它。出于某种原因,作为字符串进行比较在比较时可以正常工作,否则无法匹配。

@AgustinL.Lacuara 您无法比较 JS 中的复杂数据类型。 a={};b={}; a==b 是 false。但是在 a=b 之后它变成了 true,因为它不仅相同而且是同一个对象。

做这项工作,但是,这违背了任何良好的编程习惯。在巴西,我们称之为“冈比亚拉”

答3:

HuntsBot周刊–不定时分享成功产品案例,学习他们如何成功建立自己的副业–huntsbot.com

使用 jQuery,您可以使用 extend 浅拷贝:

var copiedObject = jQuery.extend({}, originalObject)

对 copiedObject 的后续更改不会影响 originalObject,反之亦然。

或者制作一个深拷贝:

var copiedObject = jQuery.extend(true, {}, originalObject)

答4:

huntsbot.com精选全球7大洲远程工作机会,涵盖各领域,帮助想要远程工作的数字游民们能更精准、更高效的找到对方。

在 ECMAScript 6 中有 Object.assign 方法,它将所有可枚举自身属性的值从一个对象复制到另一个对象。例如:

var x = {myProp: "value"};
var y = Object.assign({}, x); 

但请注意,这是一个浅拷贝 - 嵌套对象仍被复制为引用。

答5:

huntsbot.com精选全球7大洲远程工作机会,涵盖各领域,帮助想要远程工作的数字游民们能更精准、更高效的找到对方。

每MDN:

如果你想要浅拷贝,使用 Object.assign({}, a)

对于“深度”复制,使用 JSON.parse(JSON.stringify(a))

不需要外部库,但您需要检查 browser compatibility first。

答6:

huntsbot.com高效搞钱,一站式跟进超10+任务平台外包需求

有很多答案,但没有一个提到 ECMAScript 5 中的 Object.create,它诚然没有给你一个精确的副本,而是将源设置为新对象的原型。

因此,这不是问题的确切答案,但它是一种单线解决方案,因此很优雅。它最适合两种情况:

这种继承是有用的(呃!)源对象不会被修改的地方,因此这两个对象之间的关系不成问题。

例子:

var foo = { a : 1 };
var bar = Object.create(foo);
foo.a; // 1
bar.a; // 1
foo.a = 2;
bar.a; // 2 - prototype changed
bar.a = 3;
foo.a; // Still 2, since setting bar.a makes it an "own" property

为什么我认为这个解决方案更好?它是原生的,因此没有循环,没有递归。但是,较旧的浏览器将需要一个 polyfill。

这是原型继承,而不是克隆。这些是完全不同的事情。新对象没有任何它自己的属性,它只是指向原型的属性。克隆的目的是创建一个新对象,它不引用另一个对象中的任何属性。

答7:

huntsbot.com聚合了超过10+全球外包任务平台的外包需求,寻找外包任务与机会变的简单与高效。

一种在一行代码中克隆 Javascript 对象的优雅方法

Object.assign 方法是 ECMAScript 2015 (ES6) 标准的一部分,可以满足您的需要。

var clone = Object.assign({}, obj);

Object.assign() 方法用于将所有可枚举自身属性的值从一个或多个源对象复制到目标对象。

Read more…

支持旧浏览器的 polyfill:

if (!Object.assign) {
  Object.defineProperty(Object, 'assign', {
    enumerable: false,
    configurable: true,
    writable: true,
    value: function(target) {
      'use strict';
      if (target === undefined || target === null) {
        throw new TypeError('Cannot convert first argument to object');
      }

      var to = Object(target);
      for (var i = 1; i < arguments.length; i++) {
        var nextSource = arguments[i];
        if (nextSource === undefined || nextSource === null) {
          continue;
        }
        nextSource = Object(nextSource);

        var keysArray = Object.keys(nextSource);
        for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
          var nextKey = keysArray[nextIndex];
          var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
          if (desc !== undefined && desc.enumerable) {
            to[nextKey] = nextSource[nextKey];
          }
        }
      }
      return to;
    }
  });
}

这只会执行一个浅的“克隆”

我很难理解 objA = objB; 会导致各种头痛。这似乎已经解决了这个问题,至少现在......

答8:

HuntsBot周刊–不定时分享成功产品案例,学习他们如何成功建立自己的副业–huntsbot.com

互联网上的大多数解决方案都存在几个问题。所以我决定进行跟进,其中包括为什么不应该接受已接受的答案。

起始情况

我想深拷贝 Javascript Object 及其所有子级及其子级等。但由于我不是一个普通的开发者,我的 Object 有普通 properties、circular structures 甚至 nested objects。

因此,让我们先创建一个 circular structure 和一个 nested object。

function Circ() {
    this.me = this;
}

function Nested(y) {
    this.y = y;
}

让我们将所有内容放在一个名为 a 的 Object 中。

var a = {
    x: 'a',
    circ: new Circ(),
    nested: new Nested('a')
};

接下来,我们要将 a 复制到名为 b 的变量中并对其进行变异。

var b = a;

b.x = 'b';
b.nested.y = 'b';

您知道这里发生了什么,因为如果不是,您甚至不会遇到这个好问题。

console.log(a, b);

a --> Object {
    x: "b",
    circ: Circ {
        me: Circ { ... }
    },
    nested: Nested {
        y: "b"
    }
}

b --> Object {
    x: "b",
    circ: Circ {
        me: Circ { ... }
    },
    nested: Nested {
        y: "b"
    }
}

现在让我们找到解决方案。

JSON

我尝试的第一次尝试是使用 JSON。

var b = JSON.parse( JSON.stringify( a ) );

b.x = 'b';
b.nested.y = 'b';

不要在上面浪费太多时间,你会得到 TypeError: Converting circular structure to JSON。

递归副本(接受的“答案”)

让我们看看接受的答案。

function cloneSO(obj) {
    // Handle the 3 simple types, and null or undefined
    if (null == obj || "object" != typeof obj) return obj;

    // Handle Date
    if (obj instanceof Date) {
        var copy = new Date();
        copy.setTime(obj.getTime());
        return copy;
    }

    // Handle Array
    if (obj instanceof Array) {
        var copy = [];
        for (var i = 0, len = obj.length; i < len; i++) {
            copy[i] = cloneSO(obj[i]);
        }
        return copy;
    }

    // Handle Object
    if (obj instanceof Object) {
        var copy = {};
        for (var attr in obj) {
            if (obj.hasOwnProperty(attr)) copy[attr] = cloneSO(obj[attr]);
        }
        return copy;
    }

    throw new Error("Unable to copy obj! Its type isn't supported.");
}

看起来不错吧?它是对象的递归副本,也可以处理其他类型,例如 Date,但这不是必需的。

var b = cloneSO(a);

b.x = 'b';
b.nested.y = 'b';

递归和 circular structures 不能很好地协同工作… RangeError: Maximum call stack size exceeded

本机解决方案

在与同事争吵后,我的老板问我们发生了什么,他在谷歌上搜索后找到了一个简单的解决方案。它称为 Object.create。

var b = Object.create(a);

b.x = 'b';
b.nested.y = 'b';

该解决方案是前段时间添加到 Javascript 中的,甚至可以处理 circular structure。

console.log(a, b);

a --> Object {
    x: "a",
    circ: Circ {
        me: Circ { ... }
    },
    nested: Nested {
        y: "b"
    }
}

b --> Object {
    x: "b",
    circ: Circ {
        me: Circ { ... }
    },
    nested: Nested {
        y: "b"
    }
}

…你看,它不适用于内部的嵌套结构。

原生解决方案的 polyfill

与 IE 8 一样,旧版浏览器中有一个用于 Object.create 的 polyfill。它类似于 Mozilla 推荐的东西,当然,它并不完美,并且会导致与 本机解决方案相同的问题。

function F() {};
function clonePF(o) {
    F.prototype = o;
    return new F();
}

var b = clonePF(a);

b.x = 'b';
b.nested.y = 'b';

我已将 F 放在范围之外,因此我们可以看看 instanceof 告诉我们什么。

console.log(a, b);

a --> Object {
    x: "a",
    circ: Circ {
        me: Circ { ... }
    },
    nested: Nested {
        y: "b"
    }
}

b --> F {
    x: "b",
    circ: Circ {
        me: Circ { ... }
    },
    nested: Nested {
        y: "b"
    }
}

console.log(typeof a, typeof b);

a --> object
b --> object

console.log(a instanceof Object, b instanceof Object);

a --> true
b --> true

console.log(a instanceof F, b instanceof F);

a --> false
b --> true

与本机解决方案相同的问题,但输出稍差一些。

更好(但不完美)的解决方案

在四处挖掘时,我发现了一个与这个问题类似的问题 (In Javascript, when performing a deep copy, how do I avoid a cycle, due to a property being “this”?),但有一个更好的解决方案。

function cloneDR(o) {
    const gdcc = "__getDeepCircularCopy__";
    if (o !== Object(o)) {
        return o; // primitive value
    }

    var set = gdcc in o,
        cache = o[gdcc],
        result;
    if (set && typeof cache == "function") {
        return cache();
    }
    // else
    o[gdcc] = function() { return result; }; // overwrite
    if (o instanceof Array) {
        result = [];
        for (var i=0; i Object {
    x: "a",
    circ: Object {
        me: Object { ... }
    },
    nested: Object {
        y: "a"
    }
}

b --> Object {
    x: "b",
    circ: Object {
        me: Object { ... }
    },
    nested: Object {
        y: "b"
    }
}

console.log(typeof a, typeof b);

a --> object
b --> object

console.log(a instanceof Object, b instanceof Object);

a --> true
b --> true

console.log(a instanceof F, b instanceof F);

a --> false
b --> false

要求是匹配的,但还有一些小问题,包括将 nested 的 instance 和 circ 更改为 Object。

共享叶子的树的结构不会被复制,它们将成为两个独立的叶子:

        [Object]                     [Object]
         /    \                       /    \
        /      \                     /      \
      |/_      _\|                 |/_      _\|  
  [Object]    [Object]   ===>  [Object]    [Object]
       \        /                 |           |
        \      /                  |           |
        _\|  |/_                 \|/         \|/
        [Object]               [Object]    [Object]

结论

最后一个使用递归和缓存的解决方案可能不是最好的,但它是对象的真实深层副本。它处理简单的 properties、circular structures 和 nested object,但在克隆时会弄乱它们的实例。

jsfiddle

所以结论是避免这个问题:)

@mikus 直到有一个真正的规范,它不仅涵盖基本用例,是的。

对上面提供的解决方案进行了很好的分析,但作者得出的结论表明这个问题没有解决方案。

遗憾的是 JS 不包含原生克隆功能。

在所有最佳答案中,我觉得这接近正确的答案。

答9:

huntsbot.com高效搞钱,一站式跟进超10+任务平台外包需求

如果您对浅拷贝没问题,underscore.js 库有一个 clone 方法。

y = _.clone(x);

或者你可以像这样扩展它

copiedObject = _.extend({},originalObject);

谢谢。在 Meteor 服务器上使用此技术。

要快速开始使用 lodash,我建议学习 npm、Browserify 以及 lodash。我让克隆与 'npm i --save lodash.clone' 一起工作,然后是 'var clone = require('lodash.clone');'要让 require 工作,你需要类似 browserify 的东西。安装并了解它的工作原理后,您将在每次运行代码时使用“browserify yourfile.js > bundle.js;start chrome index.html”(而不是直接进入 Chrome)。这会将您的文件和 npm 模块所需的所有文件收集到 bundle.js 中。不过,您可能可以使用 Gulp 节省时间并自动执行此步骤。

答10:

HuntsBot周刊–不定时分享成功产品案例,学习他们如何成功建立自己的副业–huntsbot.com

好的,假设你在下面有这个对象并且你想克隆它:

let obj = {a:1, b:2, c:3}; //ES6

或者

var obj = {a:1, b:2, c:3}; //ES5

答案主要取决于您使用的 ECMAscript,在 ES6+ 中,您可以简单地使用 Object.assign 进行克隆:

let cloned = Object.assign({}, obj); //new {a:1, b:2, c:3};

或像这样使用扩展运算符:

let cloned = {...obj}; //new {a:1, b:2, c:3};

但是如果你使用 ES5,你可以使用几个方法,但是 JSON.stringify,只要确保你不使用大量数据来复制,但在很多情况下它可能是一个方便的方式,像这样:

let cloned = JSON.parse(JSON.stringify(obj)); 
//new {a:1, b:2, c:3};, can be handy, but avoid using on big chunk of data over and over

您能否举例说明 big chunk of data 的含义? 100KB? 100MB?谢谢!

是的,@ user1063287,基本上更大的数据,性能更差......所以这真的取决于,而不是kb,mb或gb,更多的是你想要做多少次......而且它也行不通对于功能和其他东西......

Object.assign 制作浅拷贝(就像传播一样,@Alizera)

你不能在 es5 中使用 let :^) @Alireza

答11:

一个优秀的自由职业者,应该有对需求敏感和精准需求捕获的能力,而huntsbot.com提供了这个机会

2020 年 7 月 6 日更新

有三 (3) 种方法可以在 JavaScript 中克隆对象。由于 JavaScript 中的对象是引用值,因此您不能简单地使用 = 进行复制。

方法是:

const food = { food: 'apple', drink: 'milk' }


// 1. Using the "Spread"
// ------------------

{ ...food }


// 2. Using "Object.assign"
// ------------------

Object.assign({}, food)


// 3. "JSON"
// ------------------

JSON.parse(JSON.stringify(food))

// RESULT:
// { food: 'apple', drink: 'milk' }


这可以作为参考总结。

这为这个问题增加了哪些新的/独特的信息?

JSON 方法将删除对象的任何方法

从一个对象创建一个字符串,然后将该字符串解析为另一个对象只是为了复制该对象是一种 Monty Python 的编程风格:-D

这仅适用于对象字面量和可以这样表示的对象,但不适用于您在 OO 语言中遇到的通用“对象”。这似乎是 OP 要求的,因此没关系,但它不是适用于每种对象的通用解决方案。

对于具有层次结构的对象,扩展运算符和 Object.assign 失败,即。嵌套对象。 JSON.parse/stringify 有效,但如上所述不复制方法。

原文链接:https://www.huntsbot.com/qa/rDo0/how-do-i-correctly-clone-a-javascript-object?lang=zh_CN&from=csdn

保持自己快人一步,享受全网独家提供的一站式外包任务、远程工作、创意产品订阅服务–huntsbot.com

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值