Клонування масиву в Javascript / Typescript


79

У мене є масив із двох об’єктів:

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

Я заповнюю свою таблицю HTML genericItemsданими. Таблицю можна змінювати. Існує кнопка скидання, щоб скасувати всі внесені зміни backUpData. Цей масив заповнюється службою:

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

Моя ідея полягала в тому, що зміни користувача відображатимуться у першому масиві, а другий масив може бути використаний як резервна копія для операції скидання. Проблема, з якою я стикаюся тут, полягає в тому, коли користувач змінює таблицю ( genericItems[])другий масив backupDataтакож модифікується.

Як це відбувається і як цьому запобігти?


Схоже, ви зробили неглибоку копію масиву. Здається, ви модифікуєте об’єкти, які вони тримали, і бачите зміни. Вам потрібно зробити глибоку копію або запропонувати інший спосіб представлення ваших даних.
Джефф Меркадо

Вони вказують на одне і те ж посилання. Якщо ви повернете новий масив за допомогою бібліотеки, як-от lodash чи подібної, у вас не виникне цієї проблеми.
Латинський воїн

slice()створить новий об’єкт з іншого масиву, я здогадуюсь ...
Арун

Другий масив модифікується, оскільки замість створення нового масиву ви просто посилаєтесь на вихідний. Якщо ви використовуєте сценарій типу та ES6, ви можете створити таку копію, як this this.backupData = [... this.genericItems] це створить копію масиву. Сподіваюся, це допоможе!
Молік Мія

2
@MolikMiah Я кажу, що sliceбере масив і копіює кожне посилання в новий масив. Тож старий і новий масив насправді різняться, але об’єкти всередині абсолютно однакові. Отже, це має бути те саме, що і робити[...array]
Frank Modica

Відповіді:


166

Клонувати об’єкт:

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

13
Якщо ваш масив - це масив об’єктів (не примітивних типів), то вам потрібно заглибитися на один рівень за допомогою неглибокої копії. Для мене рішенням було перебирати масив та клонувати об’єкти. Тобто const myArray= [{ a: 'a', b: 'b' }, { a: 'c', b: 'd' }]; const myClonedArray = []; myArray.map(val => myClonedArray.push(Object.assign({}, val)));. Альтернативним рішенням для належної глибокої копії буде серіалізація JSON, як зазначено в інших відповідях.
бурмоче NZ

@mumblesNZ, якщо ви дійсно говорите про глибоку копію, двох рівнів теж буде недостатньо. Вам довелося б використовувати щось на зразок Лодаша_.cloneDeep(obj) . Як ви сказали, серіалізація JSON спрацює, але це дуже обхідний спосіб зробити це.
user2683747

67

Клонування масивів та об'єктів у javascript має інший синтаксис . Рано чи пізно всі пізнають різницю важким шляхом і опиняються тут.

У друкованому та ES6 ви можете використовувати оператор поширення для масиву і об'єкта:

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}}]

1
Ах! Пенні падає! Час припинити використовувати оператор розповсюдження для клонування масивів об'єктів
sidonaldson

const myClonedArray = [... myArray] працює для [{a: 1}, {b: 2}].
Суяш Гупта,

2
Ні, це не є. спробуйте змінити ваш клонований елемент, і він також змінить оригінал. у вас є копія за посиланням, а не за значенням.
Девід

1
Для глибокого клонування також можна використовувати "Object.assign (Object.create (Object.getPrototypeOf (obj)), obj);" замість використання зовнішньої бібліотеки.
CoderApprentice

18

Використання карти або іншого подібного рішення не допомагає глибоко клонувати масив об’єктів. Більш простий спосіб зробити це без додавання нової бібліотеки - використання JSON.stringfy, а потім JSON.parse.

У вашому випадку це має спрацювати:

this.backupData = JSON.parse(JSON.stringify(genericItems));
  • При використанні останньої версії JS / TS встановлення великої бібліотеки, як lodash, лише для однієї / двох функцій - поганий крок. Вам сподобається продуктивність вашого додатка, і в перспективі вам доведеться підтримувати оновлення бібліотеки! перевірте https://bundlephobia.com/result?p=lodash@4.17.15
  • Для малого objet lodash cloneDeep може бути швидшим, але для більшого / глибшого об'єкта клон json стає швидшим. Тож у цьому випадку вам не соромтеся використовувати його. перевірити https://www.measurethat.net/Benchmarks/Show/6039/0/lodash-clonedeep-vs-json-clone-larger-object та інформацію про https://v8.dev/blog/cost-of-javascript -2019 # json

  • Незручно те, що ваш вихідний об'єкт повинен бути конвертованим у JSON.



4

Наступний рядок у вашому коді створює новий масив, копіює всі посилання 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());

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)що не працює.



1

Спробуйте це:

[https://lodash.com/docs/4.17.4#clone][1]

var objects = [{ 'a': 1 }, { 'b': 2 }];

var shallow = _.clone(objects);
console.log(shallow[0] === objects[0]);
// => true

1

Схоже, ви, можливо, допустили помилку щодо того, де ви робите копію масиву. Погляньте на моє пояснення нижче та невелику модифікацію коду, яка допоможе вам скинути дані до попереднього стану.

У вашому прикладі я бачу наступне:

  • ви робите запит на отримання загальних предметів
  • після отримання даних ви встановлюєте результати для this.genericItems
  • безпосередньо після цього ви встановлюєте backupData як результат

Я не помиляюся, думаючи, що ви не хочете, щоб 3-й пункт відбувся в такому порядку?

Чи було б краще:

  • ви робите запит даних
  • зробіть резервну копію поточного в this.genericItems
  • потім встановіть genericItems як результат вашого запиту

Спробуйте це:

getGenericItems(selected: Item) {
  this.itemService.getGenericItems(selected).subscribe(
    result => {
       // make a backup before you change the genericItems
       this.backupData = this.genericItems.slice();

       // now update genericItems with the results from your request
       this.genericItems = result;
    });
  }

1

Нижче наведений код може допомогти вам скопіювати об'єкти першого рівня

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 23 -- lost the original one :(

Остаточна порада:

Я б сказав, перейдіть до cloneDeepAPI lodash, який допомагає копіювати об'єкти всередині об'єктів, повністю розмежовуючи оригінальний. Це можна встановити як окремий модуль.

Довідкова документація: https://github.com/lodash/lodash

Індивідуальний пакет : https://www.npmjs.com/package/lodash.clonedeep


1

найпростіший спосіб клонування масиву -

backUpData = genericItems.concat();

Це створить нову пам’ять для індексів масиву


Це не створює нової пам'яті для backUpData. backUpDataдосі зберігає посилання на genericItems.
Суяш Гупта,


0

Схоже, що ви хочете - це глибока копія об’єкта. Чому б не використовувати Object.assign()? Не потрібні бібліотеки, і це однокласний :)

getGenericItems(selected: Item) {
    this.itemService.getGenericItems(selected).subscribe(
        result => {
            this.genericItems = result;
            this.backupDate = Object.assign({}, result); 
            //this.backupdate WILL NOT share the same memory locations as this.genericItems
            //modifying this.genericItems WILL NOT modify this.backupdate
        });
}

Детальніше на Object.assign(): https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/ assign


Це буде робити те саме, що і slice(). Він створює новий масив, але копіює посилання на об'єкти зі старого масиву.
Frank Modica

Крім того, я думаю, це повинно бути Object.assign([], result). В іншому випадку, я думаю, ви втратите lengthвласність (а може і деякі інші речі).
Frank Modica

-1

Спробуйте це

const returnedTarget = Object.assign(target, source);

і передайте порожній масив цілі

у випадку, якщо складні об'єкти таким чином працюють для мене

$.extend(true, [], originalArray) у випадку масиву

$.extend(true, {}, originalObject) у випадку об’єкта

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.