Клонування масиву в 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.