在Javascript/Typescript中克隆一个数组

161

我有一个包含两个对象的数组:

genericItems: Item[] = [];
backupData: Item[] = [];

我正在使用genericItems数据填充我的HTML表格。该表格是可修改的。有一个重置按钮可以撤消使用backUpData所做的所有更改。这个数组由一个服务填充:

getGenericItems(selected: Item) {
this.itemService.getGenericItems(selected).subscribe(
  result => {
     this.genericItems = result;
  });
     this.backupData = this.genericItems.slice();
  }

我的想法是,用户的更改将会反映在第一个数组中,而第二个数组可以用作重置操作的备份。我面临的问题是,当用户修改表格(<code>genericItems[]</code>)时,第二个数组<code>backupData</code>也会被修改。

这是怎么发生的,如何防止这种情况?


1
看起来你制作了一个数组的浅拷贝。听起来你正在修改它们所持有的对象并看到了变化。你需要制作一个深拷贝或者想出一种不同的方式来表示你的数据。 - Jeff Mercado
1
它们指向同一引用。如果您使用类似lodash的库返回一个新数组,您就不会遇到这个问题。 - Latin Warrior
slice() 会从另一个数组创建一个新的对象,我猜是这样... - Arun
1
第二个数组被修改了,因为你没有创建一个新的数组,而是仅仅引用了原来的数组。如果你使用 TypeScript 和 ES6,你可以像这样创建一个副本: this.backupData = [...this.genericItems],这将创建一个数组的副本。希望这能帮到你! - Molik Miah
3
@MolikMiah 我的意思是 slice 函数会将数组中的每个引用复制到一个新的数组中。因此,旧数组和新数组实际上是不同的,但其中的对象完全相同。所以这应该与使用 [...array] 是一样的。 - Frank Modica
显示剩余6条评论
16个回答

256

克隆一个对象:

const myClonedObject = Object.assign({}, myObject);

克隆一个数组:

  • 选项1 - 如果你有一个由基本数据类型组成的数组:

const myClonedArray = Object.assign([], myArray);

  • 选项2 - 如果你有一个由对象组成的数组:
const myArray= [{ a: 'a', b: 'b' }, { a: 'c', b: 'd' }];
const myClonedArray = [];
myArray.forEach(val => myClonedArray.push(Object.assign({}, val)));

15
如果你的数组是包含对象的数组(而非基本数据类型),那么你需要在进行浅拷贝时多深入一层。我的解决方案是遍历数组并克隆对象,如下所示:const myArray= [{ a: 'a', b: 'b' }, { a: 'c', b: 'd' }]; const myClonedArray = []; myArray.map(val => myClonedArray.push(Object.assign({}, val)));. 另外一个可行的深拷贝方案是使用JSON序列化,正如其他答案中提到的。 - mumblesNZ
3
@mumblesNZ,如果你真的在谈论深拷贝,两个级别也不够。你需要使用像 Lodash 的 _.cloneDeep(obj) 这样的东西。JSON 序列化也可以,就像你说的那样,但这是一种非常迂回的方法。 - user2683747
1
这个回答的第二部分对于保存原始数据类型的对象可行。如果数组元素包含一个数组或对象类型的值,那么如@user2683747所提到的,深度复制是有帮助的。 - Deepak
1
一个关于深拷贝和浅拷贝的有用参考。https://www.techmeet360.com/blog/playing-with-javascript-object-clone/ - Deepak

145

在javascript中克隆数组和对象的语法不同,迟早每个人都会通过艰难的方式学习到这种差异并最终到达这里。

TypescriptES6中,您可以使用展开运算符来处理数组和对象:

const myClonedArray  = [...myArray];  // This is ok for [1,2,'test','bla']
                                      // But wont work for [{a:1}, {b:2}]. 
                                      // A bug will occur when you 
                                      // modify the clone and you expect the 
                                      // original not to be modified.
                                      // The solution is to do a deep copy
                                      // when you are cloning an array of objects.

要深度复制一个对象,你需要使用外部库:

import {cloneDeep} from 'lodash';
const myClonedArray = cloneDeep(myArray);     // This works for [{a:1}, {b:2}]

展开运算符也适用于对象,但它仅会执行浅拷贝(只复制第一层子元素)。
const myShallowClonedObject = {...myObject};   // Will do a shallow copy
                                               // and cause you an un expected bug.

要深度复制一个对象,你需要使用外部库:

 import {cloneDeep} from 'lodash';
 const deeplyClonedObject = cloneDeep(myObject);   // This works for [{a:{b:2}}]

3
啊!恍然大悟!是时候停止使用扩展运算符来克隆对象数组了。 - sidonaldson
const myClonedArray = [...myArray] 对于 [{a:1}, {b:2}] 是有效的。 - Suyash Gupta
7
不,它并不会。尝试修改你克隆的项目,它也会修改原始项目。你拥有的是按引用而非按值复制。 - David Dehghan
2
对于深度克隆,也可以使用“Object.assign(Object.create(Object.getPrototypeOf(obj)), obj);”而不是使用外部库。 - CoderApprentice
这是适用于数组的数组的解决方案。 - makkasi

37

使用 map 或其他类似的解决方案不能深度复制对象数组。 不需要添加新库,更简单的方法是使用 JSON.stringify,然后再使用 JSON.parse。

在您的情况下,应该这样操作:

this.backupData = JSON.parse(JSON.stringify(genericItems));

在不使用任何新库的情况下,深度克隆对象数组的最佳解决方案 - EssaidiM

19

请尝试以下代码:

this.cloneArray= [...this.OriginalArray]

虽然展开运算符很有帮助,但如果数组包含对象,则它只会进行浅拷贝。 - Nicholas Smith

6
你的代码中以下这行代码创建了一个新的数组,将genericItems中所有对象的引用复制到这个新数组中,并将其赋值给backupData
this.backupData = this.genericItems.slice();

因此,尽管 backupData genericItems 是不同的数组,它们包含完全相同的对象引用。

您可以引入一个库来为您执行深层复制(如@LatinWarrior所提到的)。

但是,如果 Item 不太复杂,也许您可以添加一个 clone 方法来自行深度克隆对象:

class Item {
  somePrimitiveType: string;
  someRefType: any = { someProperty: 0 };

  clone(): Item {
    let clone = new Item();

    // Assignment will copy primitive types

    clone.somePrimitiveType = this.somePrimitiveType;

    // Explicitly deep copy the reference types

    clone.someRefType = {
      someProperty: this.someRefType.someProperty
    };

    return clone;
  }
}

然后在每个项目上调用 clone():
this.backupData = this.genericItems.map(item => item.clone());

6

数组复制解释 - 深拷贝与浅拷贝

下面的代码可能会帮助你复制第一层对象

let original = [{ a: 1 }, {b:1}]
const copy = [ ...original ].map(item=>({...item}))

所以对于下面的情况,数值保持不变。
copy[0].a = 23
console.log(original[0].a) //logs 1 -- value didn't change voila :)

对于这种情况失败

let original = [{ a: {b:2} }, {b:1}]
const copy = [ ...original ].map(item=>({...item}))
copy[0].a.b = 23;
console.log(original[0].a) //logs {b: 23} -- lost the original one :(

尝试使用lodash单独的ES模块-cloneDeep:
我会建议你使用lodash的cloneDeep API(这可以作为一个单独的模块安装,减少代码占用空间以便进行树摇),它可以帮助你完全复制对象内的对象,并与原始对象解除引用。
作为另一种选择,你也可以依赖于JSON.stringify和JSON.parse方法来快速解除引用并保持性能。
请参考文档:https://github.com/lodash/lodash 单独的包:https://www.npmjs.com/package/lodash.clonedeep

5

非常强大的克隆(不带引用)对象/数组

您可以使用@angular-devkit获取对象/数组的深复制。

import { deepCopy } from '@angular-devkit/core/src/utils/object';

export class AppComponent {

  object = { .. some object data .. }
  array = [ .. some list data .. ]

  constructor() {
     const newObject = deepCopy(this.object);
     const newArray = deepCopy(this.array);
  }
}

2
很棒,这个可以用,这是一个本地解决方案,适合那些不喜欢或没有“loaddash”依赖的人。 - HariHaran
1
搞定了,非常感谢。我很惊讶为什么展开运算符或其他方法如切片等都不起作用。将目标数组分配给源数组总是会改变源数组,真是一场灾难。 - Nexus
@Nexus "对象"(对象指的是{ ... })的数组实际上并不是一个对象的数组。这是微妙的,但是一个对象的数组实际上是一个引用的数组。以现实世界的比喻来说,将"对象"看作是网络上的网站:在这种情况下,"对象的数组"实际上是一个URL地址的数组。数组并不包含网站本身,如果你复制这个数组,你将得到URL的副本。如果一个网站的所有者更改了他们的主页,你将从两个数组中都"看到"这个变化。(续...) - undefined
网站(对象)没有被复制,因为它们一开始就不在数组中。这些对象在其他地方(计算机内存)-你不需要知道,因为javascript引擎会跟踪它们-就像一个网站存在于某个服务器的物理地址上-但你不需要知道具体位置,因为你可以通过URL引用它。显然,如果你想复制网站本身,你需要更多的代码来完成这个任务,这就是为什么其他聪明人开发了这样的“deepCopy”方法。 - undefined

5
你可以使用map函数。
 toArray= fromArray.map(x => x);

3

我也有与 primeNg DataTable 相同的问题。 经过多次尝试和哭泣后,我使用了这段代码解决了问题。

private deepArrayCopy(arr: SelectItem[]): SelectItem[] {
    const result: SelectItem[] = [];
    if (!arr) {
      return result;
    }
    const arrayLength = arr.length;
    for (let i = 0; i <= arrayLength; i++) {
      const item = arr[i];
      if (item) {
        result.push({ label: item.label, value: item.value });
      }
    }
    return result;
  }

用于初始化备份数值

backupData = this.deepArrayCopy(genericItems);

重置更改的方法如下:
genericItems = this.deepArrayCopy(backupData);

魔法弹药是使用 {} 重新创建项目,而不是调用构造函数。 我尝试过 new SelectItem(item.label, item.value),但它没有起作用。

2

克隆数组最简单的方法是

backUpData = genericItems.concat();

这将为数组索引创建一个新的内存。

这不会为backUpData创建新的内存。 backUpData仍然持有genericItems的引用。 - Suyash Gupta

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