
Существует несколько способов клонировать объект или массив в 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.