Как клонировать массив объектов в Javascript?

... где каждый объект также имеет ссылки на другие объекты в том же массиве?

Когда я впервые столкнулся с этой проблемой, я подумал о чем-то вроде

var clonedNodesArray = nodesArray.clone()

будет существовать и искал информацию о том, как клонировать объекты в javascript. Я нашел вопрос о StackOverflow (ответил тот же @JohnResig), и он указал, что с jQuery вы можете сделать

var clonedNodesArray = jQuery.extend({}, nodesArray);

клонировать объект. Я пробовал это, но это копирует только ссылки на объекты в массиве. Так что если я

nodesArray[0].value = "red"
clonedNodesArray[0].value = "green"

значение обоих nodesArray [0] и clonedNodesArray [0] окажется «зеленым». Потом я попробовал

var clonedNodesArray = jQuery.extend(true, {}, nodesArray);

который глубоко копирует объект, но я получил сообщения слишком много рекурсии » и переполнение стека управления » от Firebug и Opera Dragonfly соответственно.

Как бы ты это сделал? Неужели этого даже не следует делать? Есть ли многоразовый способ сделать это в Javascript?

Ответов (25)

Решение

Проблема с вашей неглубокой копией в том, что не все объекты клонированы. Хотя ссылки на каждый объект уникальны в каждом массиве, как только вы в конечном итоге захватите его, вы будете иметь дело с тем же объектом, что и раньше. Нет ничего плохого в том, как вы его клонировали ... тот же результат будет с использованием Array.slice ().

Причина, по которой у вашей глубокой копии возникают проблемы, заключается в том, что вы получаете круговые ссылки на объекты. Deep будет идти настолько глубоко, насколько это возможно, и если у вас есть круг, он будет продолжаться бесконечно, пока браузер не потеряет сознание.

Если структура данных не может быть представлена ​​в виде ориентированного ациклического графа, я не уверен, что вы сможете найти универсальный метод для глубокого клонирования. Циклические графы предоставляют множество сложных угловых случаев, и, поскольку это не обычная операция, я сомневаюсь, что кто-то написал полное решение (если это вообще возможно - может и не быть! Но сейчас у меня нет времени пытаться написать строгое доказательство). На этой странице я нашел несколько хороших комментариев по этому поводу .

Если вам нужна глубокая копия массива объектов с круговыми ссылками, я считаю, что вам придется закодировать свой собственный метод для обработки вашей специализированной структуры данных, так что это будет многопроходный клон:

  1. В первом раунде создайте клон всех объектов, которые не ссылаются на другие объекты в массиве. Следите за происхождением каждого объекта.
  2. Во втором раунде соедините объекты вместе.

Это работает для меня:

var clonedArray = $.map(originalArray, function (obj) {
                      return $.extend({}, obj);
                  });

И если вам нужна глубокая копия объектов в массиве:

var clonedArray = $.map(originalArray, function (obj) {
                      return $.extend(true, {}, obj);
                  });
$.evalJSON($.toJSON(origArray));

Если все, что вам нужно, это неглубокая копия, действительно простой способ:

new_array = old_array.slice(0);

Расширение JQuery работает нормально, просто вам нужно указать, что вы клонируете массив, а не объект ( обратите внимание на [] вместо {} в качестве параметра метода расширения ):

var clonedNodesArray = jQuery.extend([], nodesArray);

Я отвечаю на этот вопрос, потому что, похоже, не существует простого и явного решения проблемы «клонирования массива объектов в Javascript»:

function deepCopy (arr) {
    var out = [];
    for (var i = 0, len = arr.length; i < len; i++) {
        var item = arr[i];
        var obj = {};
        for (var k in item) {
            obj[k] = item[k];
        }
        out.push(obj);
    }
    return out;
}

// test case

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

var copy = deepCopy(original);

// change value in copy
copy[0]['a'] = 'not 1';

// original[0]['a'] still equals 1

Это решение выполняет итерацию значений массива, затем выполняет итерацию ключей объекта, сохраняя последние в новом объекте, а затем помещая этот новый объект в новый массив.

См. Jsfiddle . Примечание: простой .slice() или [].concat() недостаточный для объектов в массиве.

Пока ваши объекты содержат JSON-сериализуемый контент (без функций, нет Number.POSITIVE_INFINITY и т. Д.), Нет необходимости в каких-либо циклах для клонирования массивов или объектов. Вот чистое однострочное решение ванили.

var clonedArray = JSON.parse(JSON.stringify(nodesArray))

Подводя итог комментариям ниже, основным преимуществом этого подхода является то, что он также клонирует содержимое массива, а не только сам массив. Основными недостатками являются ограничение работы только с JSON-сериализуемым контентом и его производительность (что значительно хуже, чем у slice основанного подхода).

Мой подход:

var temp = { arr : originalArray };
var obj = $.extend(true, {}, temp);
return obj.arr;

дает мне красивый, чистый, глубокий клон исходного массива - ни один из объектов не ссылается на оригинал :-)

Это глубоко копирует массивы, объекты, нулевые и другие скалярные значения, а также глубоко копирует любые свойства неродных функций (что довольно редко, но возможно). (Для повышения эффективности мы не пытаемся копировать нечисловые свойства в массивы.)

function deepClone (item) {
  if (Array.isArray(item)) {
    var newArr = [];
    for (var i = item.length; i-- > 0;) {
      newArr[i] = deepClone(item[i]);
    }
    return newArr;
  }
  if (typeof item === 'function' && !(/\(\) \{ \[native/).test(item.toString())) {
    var obj;
    eval('obj = '+ item.toString());
    for (var k in item) {
      obj[k] = deepClone(item[k]);
    }
    return obj;
  }
  if (item && typeof item === 'object') {
    var obj = {};
    for (var k in item) {
      obj[k] = deepClone(item[k]);
    }
    return obj;
  }
  return item;
}

Если вам нужен только неглубокий клон, лучший способ сделать этот клон выглядит следующим образом:

Использование ... оператора распространения ES6.

Вот самый простой пример:

var clonedObjArray = [...oldObjArray];

Таким образом, мы распределяем массив на отдельные значения и помещаем его в новый массив с помощью оператора [].

Вот более длинный пример, показывающий, как это работает:

let objArray = [ {a:1} , {b:2} ];

let refArray = objArray; // this will just point to the objArray
let clonedArray = [...objArray]; // will clone the array

console.log( "before:" );
console.log( "obj array" , objArray );
console.log( "ref array" , refArray );
console.log( "cloned array" , clonedArray );

objArray[0] = {c:3};

console.log( "after:" );
console.log( "obj array" , objArray ); // [ {c:3} , {b:2} ]
console.log( "ref array" , refArray ); // [ {c:3} , {b:2} ]
console.log( "cloned array" , clonedArray ); // [ {a:1} , {b:2} ]

В зависимости от того, есть ли у вас Underscore или Babel, вот эталон различных способов глубокого клонирования массива.

https://jsperf.com/object-rest-spread-vs-clone/2

Похоже, Babel самый быстрый.

var x = babel({}, obj)

Этот метод очень прост, и вы можете изменить свой клон, не изменяя исходный массив.

// Original Array
let array = [{name: 'Rafael'}, {name: 'Matheus'}];

// Cloning Array
let clone = array.map(a => {return {...a}})

// Editing the cloned array
clone[1].name = 'Carlos';


console.log('array', array)
// [{name: 'Rafael'}, {name: 'Matheus'}]

console.log('clone', clone)
// [{name: 'Rafael'}, {name: 'Carlos'}]

Я использую новый метод ECMAScript 6 Object.assign :

let oldObject = [1,3,5,"test"];
let newObject = Object.assign({}, oldObject);

первым аргументом этого метода является обновляемый массив, мы передаем пустой объект, потому что хотим иметь новый объект.

мы также можем использовать этот синтаксис, который такой же, но короче:

let newObject = [...oldObject];

Карта создаст новый массив из старого (без ссылки на старый), а внутри карты вы создадите новый объект, перебираете свойства (ключи) и присваиваете значения из старого объекта Array соответствующим свойствам новому объекту.

Это создаст точно такой же массив объектов.

let newArray = oldArray.map(a => {
               let newObject = {};
               Object.keys(a).forEach(propertyKey => {
                    newObject[propertyKey] = a[propertyKey];
               });
               return newObject ;
});

Я думаю, что удалось написать общий метод глубокого клонирования любой структуры JavaScript, в основном с использованием, Object.create который поддерживается во всех современных браузерах. Код такой:

function deepClone (item) {
  if (Array.isArray(item)) {
    var newArr = [];

    for (var i = item.length; i-- !== 0;) {
      newArr[i] = deepClone(item[i]);
    }

    return newArr;
  }
  else if (typeof item === 'function') {
    eval('var temp = '+ item.toString());
    return temp;
  }
  else if (typeof item === 'object')
    return Object.create(item);
  else
    return item;
}

Вот мое решение, оно работает для массива объектов или карты. Это решение также сохраняет свои методы.

Глубокая копия означает фактическое создание нового массива и копирование значений, поскольку все, что с ним происходит, никогда не повлияет на исходный массив.

Для меня это лучшее решение:

deepCopy(inputObj: any) {
    var newObj = inputObj;
    if (inputObj && typeof inputObj === "object") {
        newObj = Object.prototype.toString.call(inputObj) === "[object Array]" ? [] : {};
        for (var i in inputObj) {
            newObj[i] = this.deepCopy(inputObj[i]);
        }

        //For maps
        if(Object.prototype.toString.call(inputObj) === "[object Map]"){
            newObj = new Map;
            inputObj.forEach((v,k) =>{
                newObj.set(k,this.deepCopy(v));
            });
        }
    }
    return newObj;
}

Если вы хотите реализовать глубокое клонирование, используйте JSON.parse (JSON.stringify (ваш {} или []))

const myObj ={
    a:1,
    b:2,
    b:3
}

const deepClone=JSON.parse(JSON.stringify(myObj));
deepClone.a =12;
console.log("deepClone-----"+myObj.a);
const withOutDeepClone=myObj;
withOutDeepClone.a =12;
console.log("withOutDeepClone----"+myObj.a);

Решил клонирование массива объектов с помощью Object.assign

const newArray = myArray.map(a => Object.assign({}, a));

или даже короче с синтаксисом распространения

const newArray = myArray.map(a => ({...a}));

Для клонирования объектов я просто собирался предложить ECMAScript 6 reduce() :

const newArray=myArray.reduce((array, element)=>array.push(Object.assign({}, element)), []);

Но, честно говоря, мне даже больше нравится ответ @dinodsaurus. Я просто помещаю эту версию здесь как еще один вариант, но лично я буду использовать, map() как предлагает @dinodsaurus.

function deepCloneArray(array) {
    return Array.from(Object.create(array));
}

lodash имеет cloneDeep функцию для этих целей:

var objects = [{ 'a': 1 }, { 'b': 2 }];
var deep = _.cloneDeep(objects);

Array.slice можно использовать для копирования массива или части массива. http://www.devguru.com/Technologies/Ecmascript/Quickref/Slice.html Это будет работать со строками и числами .. - изменение строки в один массив не повлияет на другой, но объекты по-прежнему просто копируются по ссылке, поэтому изменения в ссылочных объектах в одном массиве повлияют на другой массив.

Вот пример диспетчера отмены JavaScript, который может быть полезен для этого: http://www.ridgway.co.za/archive/2007/11/07/simple-javascript-undo-manager-for-dtos.aspx

Как заметил Дэниел Лью, у циклических графов есть некоторые проблемы. Если бы у меня была эта проблема, я бы либо добавил специальные clone() методы к проблемным объектам, либо запомнил, какие объекты я уже скопировал.

Я бы сделал это с переменной, copyCount которая увеличивается на 1 каждый раз, когда вы копируете свой код. Копируется объект, имеющий более низкий, copyCount чем текущий процесс копирования. В противном случае следует указать ссылку на уже существующую копию. Это требует ссылки с оригинала на его копию.

Есть еще одна проблема: память. Если у вас есть эта ссылка с одного объекта на другой, вероятно, браузер не может освободить эти объекты, поскольку на них всегда откуда-то ссылаются. Вам нужно будет сделать второй проход, где вы установите для всех копируемых ссылок значение Null. (Если вы сделаете это, вам не потребуется, copyCount но isCopied будет достаточно логического значения , так как вы можете сбросить значение во втором проходе.)

У меня может быть простой способ сделать это без мучительной рекурсии и незнания всех мельчайших деталей рассматриваемого объекта. Используя jQuery, просто преобразуйте свой объект в JSON с помощью jQuery $.toJSON(myObjectArray), затем возьмите строку JSON и оцените ее обратно в объект. БАМ! Готово, готово! Задача решена. :)

var oldObjArray = [{ Something: 'blah', Cool: true }];
var newObjArray = eval($.toJSON(oldObjArray));
       var game_popularity = [
            { game: "fruit ninja", popularity: 78 },
            { game: "road runner", popularity: 20 },
            { game: "maze runner", popularity: 40 },
            { game: "ludo", popularity: 75 },
            { game: "temple runner", popularity: 86 }
        ];
        console.log("sorted original array before clonning");
        game_popularity.sort((a, b) => a.popularity < b.popularity);
        console.log(game_popularity);


        console.log("clone using object assign");
        const cl2 = game_popularity.map(a => Object.assign({}, a));
        cl2[1].game = "clash of titan";
        cl2.push({ game: "logan", popularity: 57 });
        console.log(cl2);


        //adding new array element doesnt reflect in original array
        console.log("clone using concat");
        var ph = []
        var cl = ph.concat(game_popularity);

        //copied by reference ?
        cl[0].game = "rise of civilization";

        game_popularity[0].game = 'ping me';
        cl.push({ game: "angry bird", popularity: 67 });
        console.log(cl);

        console.log("clone using ellipses");
        var cl3 = [...game_popularity];
        cl3.push({ game: "blue whale", popularity: 67 });
        cl3[2].game = "harry potter";
        console.log(cl3);

        console.log("clone using json.parse");
        var cl4 = JSON.parse(JSON.stringify(game_popularity));
        cl4.push({ game: "home alone", popularity: 87 });
        cl4[3].game ="lockhead martin";
        console.log(cl4);

        console.log("clone using Object.create");
        var cl5 = Array.from(Object.create(game_popularity));
        cl5.push({ game: "fish ville", popularity: 87 });
        cl5[3].game ="veto power";
        console.log(cl5);


        //array function
        console.log("sorted original array after clonning");
        game_popularity.sort((a, b) => a.popularity < b.popularity);
        console.log(game_popularity);


        console.log("Object.assign deep clone object array");
        console.log("json.parse deep clone object array");
        console.log("concat does not deep clone object array");
        console.log("ellipses does not deep clone object array");
        console.log("Object.create does not deep clone object array");


        Output:


        sorted original array before clonning
        [ { game: 'temple runner', popularity: 86 },
        { game: 'fruit ninja', popularity: 78 },
        { game: 'ludo', popularity: 75 },
        { game: 'maze runner', popularity: 40 },
        { game: 'road runner', popularity: 20 } ]
        clone using object assign
        [ { game: 'temple runner', popularity: 86 },
        { game: 'clash of titan', popularity: 78 },
        { game: 'ludo', popularity: 75 },
        { game: 'maze runner', popularity: 40 },
        { game: 'road runner', popularity: 20 },
        { game: 'logan', popularity: 57 } ]
        clone using concat
        [ { game: 'ping me', popularity: 86 },
        { game: 'fruit ninja', popularity: 78 },
        { game: 'ludo', popularity: 75 },
        { game: 'maze runner', popularity: 40 },
        { game: 'road runner', popularity: 20 },
        { game: 'angry bird', popularity: 67 } ]
        clone using ellipses
        [ { game: 'ping me', popularity: 86 },
        { game: 'fruit ninja', popularity: 78 },
        { game: 'harry potter', popularity: 75 },
        { game: 'maze runner', popularity: 40 },
        { game: 'road runner', popularity: 20 },
        { game: 'blue whale', popularity: 67 } ]
        clone using json.parse
        [ { game: 'ping me', popularity: 86 },
        { game: 'fruit ninja', popularity: 78 },
        { game: 'harry potter', popularity: 75 },
        { game: 'lockhead martin', popularity: 40 },
        { game: 'road runner', popularity: 20 },
        { game: 'home alone', popularity: 87 } ]
        clone using Object.create
        [ { game: 'ping me', popularity: 86 },
        { game: 'fruit ninja', popularity: 78 },
        { game: 'harry potter', popularity: 75 },
        { game: 'veto power', popularity: 40 },
        { game: 'road runner', popularity: 20 },
        { game: 'fish ville', popularity: 87 } ]
        sorted original array after clonning
        [ { game: 'ping me', popularity: 86 },
        { game: 'fruit ninja', popularity: 78 },
        { game: 'harry potter', popularity: 75 },
        { game: 'veto power', popularity: 40 },
        { game: 'road runner', popularity: 20 } ]

        Object.assign deep clone object array
        json.parse deep clone object array
        concat does not deep clone object array
        ellipses does not deep clone object array
        Object.create does not deep clone object array