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

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

Как клонировать объект или массив в 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[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, то такой вариант копирования можно считать самым простым. В противном случае необходим иной подход.

Вспомогательная функция copy()

Чтобы клонировать объект или массив наиболее удобным способом можно создать вспомогательную функцию. Назовём её 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() превосходит первый вариант по скорости почти в 3 раза.

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

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

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *