Способы клонировать объект или массив в JavaScript

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

05.09.2021
285
10 мин.
0

Объекты и массивы в JavaScript являются изменяемыми. Это означает, что их состояние может быть изменено после их создания. Тем не менее, существует несколько способов клонировать объект или массив в JavaScript. В этом случае ссылки на них отличаются от ссылок на их исходные копии.

Они сравниваются по ссылкам, а не по значениям. Этим они отличаются от строковых и числовых типов данных. Давайте рассмотрим несколько методов правильного клонирования объектов и массивов. Кроме того, мы остановимся отдельно на лучшем способе копирования массива объектов.

Использование Array.from() и Object.assign()

Первый метод предназначен для клонирования массивов, а второй — объектов.

let arr = ['hello', 'world']; 
let obj = { greeting: 'hi', name: 'universe' }; 
let arrClone = Array.from(arr); 
let objClone = Object.assign({}, obj);

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

Методы Object.assign() и Array.from() создают неглубокие копии. Вложенные массивы или объекты также не клонируются. Вместо этого создаются ссылки на их оригинал.

let deepArr = [1, 2, ['a', 'b', 'c']]; 
let deepArrClone = Array.from(deepArr);
 
// Добавление элемента во вложенный массив 
deepArrClone[2].push('d');
 
// Изменения затрагивают и оригинальный массив 
console.log(deepArr[2]); // ['a', 'b', 'c', 'd']

Клонировать объект или массив в JavaScript через спред (spread) оператор

Для копии массива или объекта можно использовать и spread оператор.

let arr = ['hello', 'world'];
let obj = {
    greeting: 'hi',
    name: 'universe'
};

let arrClone = [...arr];
let objClone = {...obj};

Тем не менее, это приводит к аналогичным проблемам, как и при применении методов Array.from() и Object.assign(), когда речь заходит о вложенных или многомерных массивах и объектах.

let deepArr = [1, 2, ['a', 'b', 'c']];
let deepArrClone = [...deepArr];

// Добавление элемента во вложенный массив
deepArrClone[2].push('d');

// Изменения затрагивают и оригинальный массив
console.log(deepArr[2]); // ['a', 'b', 'c', 'd']

Применение JSON.stringify() и JSON.parse()

Один из наиболее что рекомендуемых способов решения проблемы копирования многомерных массивов и объектов — это структурировать их при помощи метода JSON.stringify(). Затем его необходимо преобразовать обратно методом JSON.parse().

let deepArr = [1, 2, ['a', 'b', 'c']];
let deepArrClone = JSON.parse(JSON.stringify(deepArr));

deepArrClone[2].push('d');

console.log(deepArr);       // [1, 2, ['a', 'b', 'c']]
console.log(deepArrClone);  // [1, 2, ['a', 'b', 'c', 'd']]

Несмотря на то, что это вполне рабочий вариант, у него есть и недостатки. Таким способом правильно клонируются только JSON-данные.

В качестве наглядного примера рассмотрим объект, включающий объекты с несколькими типами данных. Мы клонируем его с помощью методов JSON.stringify() и JSON.parse().

let obj = {
	arr: [1, 2, 3, ['a', 'b', 'c']],
	obj: {
	    greeting: 'hi',
	    name: 'world',
	    nums: [1, 2, 3],
	    details: {
		    age: 'old',
		    letters: ['a', 'b', 'c']
	    }
	},
	str: 'hi',
	date: new Date(),
	num: 1,
	fn: function (nm) {
	    return `hi ${nm}!`;
	},
	reg: /test/i,
	bool: true,
	nl: null,
	undef: undefined,
	map: new Map([['hi', 'world'], ['hello', 'universe']]),
	set: new Set(['hi', 'world'])
};

let objClone = JSON.parse(JSON.stringify(obj));

Массивы, объекты, строки, числа, логические значения и null скопировались без проблем.

Но конструктор new Date(), функции, регулярные выражения, методы Map() и Set() подверглись изменениям. Дата превратилась в строку, а остальные данные — в пустые объекты ({}).

let objClone = {
	arr: [1, 2, 3, ['a', 'b', 'c']],
	bool: true,
	date: "2021-07-21T03:44:15.873Z",
	map: {},
	nl: null,
	num: 1,
	obj: {
	    greeting: "hi",
	    name: "world",
	    nums: [1, 2, 3],
	    details: {
                age: 'old',
	        letters: ['a', 'b', 'c']
	    }
	},
	reg: {},
	set: {},
	str: "hi"
};

Если в исходном объекте или массиве используются только данные, которые являются валидными для JSON, то такой вариант копирования можно считать самым простым. В противном случае необходим иной подход.

Способы клонировать объект или массив в JavaScript
Использование вспомогательной функции для клонирования объекта или массива в JavaScript (изображение создано с помощью ИИ)

Клонировать объект или массив в JavaScript вспомогательной функцией copy()

Давайте теперь разберёмся как клонировать объект или массив в JavaScript наиболее удобным способом. Для этого можно создать вспомогательную функцию. Назовём её copy(). Она будет перебирать каждый элемент в массиве или объекте, создавать новый объект этого типа и помещает в него все элементы из исходника. Когда она сталкивается с вложенными итерируемыми элементами, то повторяет весь этот процесс и с ними.

Результатом её работы является точная копия оригинала.

function copy (obj) {
    function copyProps (clone) {
        for (let key in obj) {
            if (Object.prototype.hasOwnProperty.call(obj, key)) {
                clone[key] = copy(obj[key]);
            }
        }
    }

    /**
    * Создание иммутабельной копии объекта
    * @return {Object}
    */
    function cloneObj () {
        let clone = {};
        copyProps(clone);
        return clone;
    }

    /**
    * Создание иммутабельной копии массива
    * @return {Array}
    */
    function cloneArr () {
        return obj.map(function (item) {
            return copy(item);
        });
    }

    /**
    * Создание иммутабельной копии Map
    * @return {Map}
    */
    function cloneMap () {
        let clone = new Map();
        for (let [key, val] of obj) {
            clone.set(key, copy(val));
        }
        return clone;
    }

    /**
    * Создание иммутабельной копии Set
    * @return {Set}
    */
    function cloneSet () {
        let clone = new Set();
        for (let item of obj) {
            clone.add(copy(item));
        }
        return clone;
    }

    /**
    * Создание иммутабельной копии функции
    * @return {Function}
    */
    function cloneFunction () {
        let clone = obj.bind(this);
        copyProps(clone);
        return clone;
    } 

    // Получение типа объекта
    let type = Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();

    // Возвращаем копию в зависимости от типа исходных данных
    if (type === 'object') return cloneObj();
    if (type === 'array') return cloneArr();
    if (type === 'map') return cloneMap();
    if (type === 'set') return cloneSet();
    if (type === 'function') return cloneFunction();

    return obj;
}

Результатом её работы является точная копия оригинала.

Тестирование производительности

Здесь следует отметить тот факт, что сочетание методов JSON.stringify() и JSON.parse() работает почти в два раза быстрее, чем вспомогательная функция copy().

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

Типы данныхcopy()Методы работы с JSON
Различные типы данных140.01 мс72.99 мс
Массивы и объекты с большим объёмом данных643.22 мс1541.07 мс

Безусловно, из этого можно сделать вывод, что использование методов Object.assign() и Array.from() наряду со spread оператором подойдёт для простых массивов и объектов.

При работе с многомерными объектами и массивами лучше прибегнуть к пользовательской функции copy(). Несмотря на то, что методы, используемые для JSON-данных в некоторых ситуациях работают быстрее, copy() гораздо более устойчива при клонировании больших объектов и массивов. Она особенно актуальна, когда речь заходит о приложениях с большими объёмами данных, которые хранятся в массивах или объектах, и которые со временем увеличиваются в объёмах.

Читайте также: Как проверить объект на пустоту в JavaScript.

Поддержка браузерами

В отличие от метода Object.assign(), который имеет широкую поддержку среди всех современных версий браузеров, Array.from() не работает в Internet Explorer.