Как объединить глубоко вложенные объекты в JavaScript
При работе в JavaScript иногда возникает потребность объединить два объекта, которые могут быть глубоко вложенными.
В этой статье мы увидим, как с этим справиться правильно.
Давайте начнем
Допустим, у нас есть такой пользовательский объект:
const user = < name: "David", phone: 122345678, location: < city: "Camden", country: "UK" >>;
И мы хотим обновить этот объект новыми значениями. Например, при обновлении своего профиля пользователя пользователь вводит новые значения, поэтому нам нужно объединить предыдущий объект с новым обновленным объектом.
Если пользователь обновит только некоторые значения, у нас будет такой объект:
const updates = < name: "David", phone: 123456789, location: < city: "Smithfield" >>;
Как вы можете видеть, объект пользователя имеет свойство location с свойствами city и country внутри него. Но в объекте обновлений у нас нет свойства страны, так как мы можем объединить эти объекты?
Попробуем известными способами.
Использование Object.assign
const user = < name: 'David', phone: 122345678, location: < city: 'Camden', country: 'UK' >>; const updates = < name: 'David', phone: 123456789, location: < city: 'Smithfield' >>; console.log(Object.assign(<>, user, updates)); /* output: < name: 'David', phone: 123456789, location: < city: 'Smithfield' >> */
Как видите, свойство location из объекта обновления полностью заменило предыдущий объект location , поэтому свойство country потеряно.
Это потому, что Object.assign выполняет неглубокое слияние. Неглубокое слияние означает, что оно объединит свойства только на первом уровне, а не на вложенном уровне.
Использование оператора распростронения
const user = < name: 'David', phone: 122345678, location: < city: 'Camden', country: 'UK' >>; const updates = < name: 'David', phone: 123456789, location: < city: 'Smithfield' >>; console.log(< . user, . updates >); /* output: < name: 'David', phone: 123456789, location: < city: 'Smithfield' >> */
Как видите, это также дает тот же результат, потому что Object.assign и оператор распространения просто неглубоко объединяют объекты.
Итак, чтобы исправить это и правильно объединить два глубоко вложенных объекта, мы можем использовать метод merge , предоставляемый очень популярной библиотекой lodash.
Использование Lodash merge
const user = < name: "David", phone: 122345678, location: < city: "Camden", country: "UK" >>; const updates = < name: "David", phone: 123456789, location: < city: "Smithfield" >>; console.log(_.merge(user, updates)); /* output: < name: 'David', phone: 123456789, location: < country: 'UK', city: 'Smithfield' >> */
Теперь все работает, как ожидалось, и country не теряется при слиянии.
Lodash — лучшая библиотека, которая имеет множество функций для реальных приложений.
Копирование объектов и ссылки
Одно из фундаментальных отличий объектов от примитивов заключается в том, что объекты хранятся и копируются «по ссылке», тогда как примитивные значения: строки, числа, логические значения и т.д. – всегда копируются «как целое значение».
Это легко понять, если мы немного заглянем под капот того, что происходит, когда мы копируем значение.
Давайте начнём с примитива, такого как строка.
Здесь мы помещаем копию message во phrase :
let message = "Привет!"; let phrase = message;
В результате мы имеем две независимые переменные, каждая из которых хранит строку «Привет!» .
Вполне очевидный результат, не так ли?
Объекты ведут себя иначе.
Переменная, которой присвоен объект, хранит не сам объект, а его «адрес в памяти» – другими словами, «ссылку» на него.
Давайте рассмотрим пример такой переменной:
let user = < name: "John" >;
И вот как это на самом деле хранится в памяти:
Объект хранится где-то в памяти (справа от изображения), в то время как переменная user (слева) имеет лишь «ссылку» на него.
Мы можем думать о переменной объекта, такой как user , как о листе бумаги с адресом объекта на нем.
Когда мы выполняем действия с объектом, к примеру, берём свойство user.name , движок JavaScript просматривает то, что находится по этому адресу, и выполняет операцию с самим объектом.
Теперь вот почему это важно.
При копировании переменной объекта копируется ссылка, но сам объект не дублируется.
let user = < name: "John" >; let admin = user; // копируется ссылка
Теперь у нас есть две переменные, каждая из которых содержит ссылку на один и тот же объект:
Как вы можете видеть, все ещё есть один объект, но теперь с двумя переменными, которые ссылаются на него.
Мы можем использовать любую переменную для доступа к объекту и изменения его содержимого:
let user = < name: 'John' >; let admin = user; admin.name = 'Pete'; // изменено по ссылке из переменной "admin" alert(user.name); // 'Pete', изменения видны по ссылке из переменной "user"
Это как если бы у нас был шкафчик с двумя ключами, и мы использовали один из них ( admin ), чтобы войти в него и внести изменения. А затем, если мы позже используем другой ключ ( user ), мы все равно открываем тот же шкафчик и можем получить доступ к изменённому содержимому.
Сравнение по ссылке
Два объекта равны только в том случае, если это один и тот же объект.
Например, здесь a и b ссылаются на один и тот же объект, поэтому они равны:
let a = <>; let b = a; // копирование по ссылке alert( a == b ); // true, обе переменные ссылаются на один и тот же объект alert( a === b ); // true
И здесь два независимых объекта не равны, даже если они выглядят одинаково (оба пусты):
let a = <>; let b = <>; // два независимых объекта alert( a == b ); // false
Для сравнений типа obj1 > obj2 или для сравнения с примитивом obj == 5 объекты преобразуются в примитивы. Очень скоро мы изучим, как работают преобразования объектов, но, по правде говоря, такие сравнения требуются очень редко и обычно они появляются в результате ошибок программиста.
Клонирование и объединение, Object.assign
Итак, копирование объектной переменной создаёт ещё одну ссылку на тот же объект.
Но что, если нам всё же нужно дублировать объект? Создать независимую копию, клон?
Это тоже выполнимо, но немного сложнее, потому что в JavaScript для этого нет встроенного метода. Но на самом деле в этом редко возникает необходимость, копирования по ссылке в большинстве случаев вполне хватает.
Но если мы действительно этого хотим, то нам нужно создать новый объект и воспроизвести структуру существующего, перебрав его свойства и скопировав их на примитивном уровне.
let user = < name: "John", age: 30 >; let clone = <>; // новый пустой объект // давайте скопируем все свойства user в него for (let key in user) < clone[key] = user[key]; >// теперь clone это полностью независимый объект с тем же содержимым clone.name = "Pete"; // изменим в нём данные alert( user.name ); // все ещё John в первоначальном объекте
Также мы можем использовать для этого метод Object.assign.
Object.assign(dest, [src1, src2, src3. ])
- Первый аргумент dest — целевой объект.
- Остальные аргументы src1, . srcN (может быть столько, сколько необходимо) являются исходными объектами
- Метод копирует свойства всех исходных объектов src1, . srcN в целевой объект dest . Другими словами, свойства всех аргументов, начиная со второго, копируются в первый объект.
- Возвращает объект dest .
Например, мы можем использовать его для объединения нескольких объектов в один:
let user = < name: "John" >; let permissions1 = < canView: true >; let permissions2 = < canEdit: true >; // копируем все свойства из permissions1 и permissions2 в user Object.assign(user, permissions1, permissions2); // теперь user =
Если скопированное имя свойства уже существует, оно будет перезаписано:
let user = < name: "John" >; Object.assign(user, < name: "Pete" >); alert(user.name); // теперь user =
Мы также можем использовать Object.assign для замены цикла for..in для простого клонирования:
let user = < name: "John", age: 30 >; let clone = Object.assign(<>, user);
Он копирует все свойства user в пустой объект и возвращает его.
Также существуют и другие методы клонирования объекта. Например, с использованием оператора расширения clone = <. user>, рассмотренного далее в учебнике.
Вложенное клонирование
До сих пор мы предполагали, что все свойства user примитивныe. Но свойства могут быть и ссылками на другие объекты. Что с ними делать?
Например, есть объект:
let user = < name: "John", sizes: < height: 182, width: 50 >>; alert( user.sizes.height ); // 182
Теперь недостаточно просто скопировать clone.sizes = user.sizes , потому что user.sizes – это объект, он будет скопирован по ссылке. Таким образом, clone и user будут иметь общий объект sizes :
let user = < name: "John", sizes: < height: 182, width: 50 >>; let clone = Object.assign(<>, user); alert( user.sizes === clone.sizes ); // true, тот же объект // user и clone обладают общим свойством sizes user.sizes.width++; // изменяем свойства в первом объекте alert(clone.sizes.width); // 51, видим результат в другом
Чтобы исправить это, мы должны использовать цикл клонирования, который проверяет каждое значение user[key] и, если это объект, тогда также копирует его структуру. Это называется «глубоким клонированием».
Мы можем реализовать глубокое клонирование, используя рекурсию. Или, чтобы не изобретать велосипед заново, возьмите готовую реализацию, например _.cloneDeep(obj) из библиотеки JavaScript lodash.
Также мы можем использовать глобальный метод structuredClone(), который позволяет сделать полную копию объекта. К сожалению он поддерживается только современными браузерами. Здесь можно ознакомиться с поддержкой этого метода.
Объекты, объявленные как константа, могут быть изменены
Важным побочным эффектом хранения объектов в качестве ссылок является то, что объект, объявленный как const , может быть изменён.
const user = < name: "John" >; user.name = "Pete"; // (*) alert(user.name); // Pete
Может показаться, что строка (*) вызовет ошибку, но, это не так. Значение user это константа, оно всегда должно ссылаться на один и тот же объект, но свойства этого объекта могут свободно изменяться.
Другими словами, const user выдаст ошибку только в том случае, если мы попытаемся задать user=. в целом.
Тем не менее, если нам действительно нужно создать постоянные свойства объекта, это тоже возможно, но с использованием совершенно других методов. Мы затронем это в главе Флаги и дескрипторы свойств.
Итого
Объекты присваиваются и копируются по ссылке. Другими словами, переменная хранит не «значение объекта», а «ссылку» (адрес в памяти) на это значение. Таким образом, копирование такой переменной или передача её в качестве аргумента функции копирует эту ссылку, а не сам объект.
Все операции с использованием скопированных ссылок (например, добавление/удаление свойств) выполняются с одним и тем же объектом.
Чтобы создать «реальную копию» (клон), мы можем использовать Object.assign для так называемой «поверхностной копии» (вложенные объекты копируются по ссылке) или функцию «глубокого клонирования», такую как _.cloneDeep(obj).
Как можно объединить свойства из нескольких объектов JavaScript?
Как объединить объекты JS, применив оператор распространения
Объединение объектов в JavaScript может происходить при использовании оператора распространения «(. )». На практике, такой способ объединить JS-объекты применяется наиболее часто из-за своей простоты принципа действия.
Его суть в том, что он никак не воздействует на оригинальные объекты, а только лишь на их основе создает другой. Давайте рассмотрим практический пример.
const worker =
name: «Дормидонт Платонович»,
age: 27
>
const ofer =
title: «Fullstack web-developer»,
location: «Удаленно»
>
const employee = <. worker, . ofer>;
console.log(employee);
Если запустить этот скрипт, то в результате мы получим:
name: «Дормидонт Платонович»,
age: 27,
title: «Fullstack web-developer»,
location: «Удаленно»
>
Что тут произошло? Мы создали 2 объекта: «worker» и «ofer», а потом их объединили в одном «employee», который просто «собрал» свой ств а от двух исходных объектов. При объединении объектов может так случиться, что они будут обладать одинаковыми свойствами, но с разными значениями. Смотрите, что произойдет тогда на примере:
const worker =
name: «Дормидонт Платонович»,
age: 27,
location: «Удаленно»
>
const ofer =
title: «Fullstack web-developer»,
location: «Офис в Москве»
>
const employee = <. worker, . ofer>;
console.log(employee);
Если запустить такой скрипт, то в результате получим следующее:
name: «Дормидонт Платонович»,
age: 27,
title: «Fullstack web-developer»,
location: «Офис в Москве»
>
Как видим, у обоих объектов есть одинаковое свойство «location», но с разными свойствами: у одного объекта «Удаленно», а у другого «Офис в Москве». В результате у объединенного объекта в этом свойстве мы получили «Офис в Москве», потому что по правилам оператора распространения при одинаковых свойствах объединяемых объектов, свойство последнего объекта «перезаписывает» свойство предыдущего. Это правило работает даже в тех случаях, когда вы хотите объединить больше двух JS-объектов.
Как объединить JS-объекты, применив специальный метод
Объединение объектов в JavaScript может происходить при помощи специальной функции «Object.assign()». Данный инструмент может объединять два и больше объектов. Он имеет довольно простой шаблон использования:
Object.assign();
Точно так же, как и в предыдущем способе, этот метод не видоизменяет оригинальные объекты, а только копирует все их свойства в новосозданном объекте. Если в исходниках попадаются обобщенные свойства, но с разными значениями, тогда значение последнего объекта «перекрывает» значение предыдущего.
Давайте рассмотрим пример:
const worker =
name: «Дормидонт Платонович»,
age: 27,
location: «Офис во Владивостоке»
>
const ofer =
title: «Fullstack web-developer»,
location: «Офис в Москве»
>
const employee = Object.assign(worker, ofer);
console.log(employee);
Если запустить этот скрипт, тогда в результате получим следующие значения:
name: «Дормидонт Платонович»,
age: 27,
title: «Fullstack web-developer»,
location: «Офис в Москве»
>
Нужно отметить, что этот метод не делает «глубокое» объединение объектов. То есть он способен точно объединить несколько JS-объектов, при условии, что в их свойствах будут располагаться примитивные значения. Если же в свойствах исходного объекта будет располагаться другой объект, тогда объединение произойдет не так, как вы можете себе представить, потому что такое значение будет передано по ссылке на исходный объект.
Еще раз отметим, что оба способа, описанных выше, в результате своей работы создают абсолютно новый объект, который копирует свойства исходных объектов.
Заключение
В JS объединить объекты довольно просто: для этого воспользуйтесь одним из способов, описанным выше. Оба способа — это инструменты, встроенные в JavaScript, но они рассчитаны для объединения объектов с простыми свойствами. Если внутри объекта в свойствах находится другой объект, тогда нужно делать «глубокое» объединение, но для этого придется воспользоваться сторонними библиотеками.
Мы будем очень благодарны
если под понравившемся материалом Вы нажмёте одну из кнопок социальных сетей и поделитесь с друзьями.
Как глубоко объединить объекты JavaScript?
Метод Object.assign или оператор распространения позволяет нам мелкие слияния объектов JavaScript.
Это означает, что объединяется только первый уровень, но более глубокие уровни по-прежнему ссылаются на исходный объект.
Глубокое слияние гарантирует, что все уровни объектов, которые мы объединяем в другой объект, будут скопированы вместо того, чтобы ссылаться на исходные объекты.
В этой статье мы рассмотрим, как выполнить глубокое слияние объектов JavaScript.
Рекурсивное слияние
Для глубокого объединения объектов JavaScript мы должны сами рекурсивно объединить каждый уровень.
Для этого нам нужно написать собственную функцию.
Например, мы можем написать:
const isObject = (item) => < return (item && typeof item === 'object' && !Array.isArray(item)); >const mergeDeep = (target, . sources) => < if (!sources.length) return target; const source = sources.shift(); if (isObject(target) && isObject(source)) < for (const key in source) < if (isObject(source[key])) < if (!target[key]) Object.assign(target, < [key]: <>>); mergeDeep(target[key], source[key]); > else < Object.assign(target, < [key]: source[key] >); > > > return mergeDeep(target, . sources); > const merged = mergeDeep(< a: 1 >, < b: < c: 123 >>); console.log(merged)
Сначала мы создаем вспомогательную функцию isObject , проверяя, является ли параметр item истинным и является ли объект не массивом.
Мы используем оператор typeof , чтобы проверить, является ли объект объектом.
Если это правда и его тип — объект, то это должен быть объект.
Затем мы проверяем, не является ли объект массивом, с помощью метода Array.isArray .
Затем мы создаем метод mergeDeep для объединения нескольких объектов в объект target .
Для этого мы сначала проверяем, хранится ли что-нибудь в sources .
Если его нет, то мы возвращаем объект target , поскольку второго и последующих аргументов нет.
Затем мы проверяем, являются ли target и source объектами.
Мы получаем source , получая первый элемент из массива source с помощью shift .
Мы перебираем ключи source с помощью цикла for-in.
Если свойство является объектом, то мы объединяем пустой объект в объект target с помощью Object.assign .
Затем мы вызываем mergeDeep рекурсивно объединяя ключи внутри свойства объекта source в объект.
В противном случае у нас есть примитивное значение свойства, и мы вызываем Object.assign для непосредственного слияния с объектом.
Наконец, в конце мы возвращаем объединенный объект.
Затем мы пробуем еще раз, вызывая mergeDeep с 2 объектами.
И мы должны увидеть, что merged это:
Метод слияния Lodash
Если мы не хотим писать нашу собственную функцию, мы можем использовать Lodash метод merge для объединения объектов.
Например, мы можем написать:
const merged = _.merge (< a: 1 >, < b: < c: 123 >>);
И мы получаем такое же значение для merged .