Хотя это может быть не самый лучший подход, его можно легко построить с помощью инструментов, которыми мы располагаем по дому. (В моем случае, с теми, которые я написал в другом ответе StackOverflow .) Я свободно использовал функции Ramda здесь, так как вопрос был помечен как Ramda (отказ от ответственности: я - автор Ramda), но ниже я показываю Альтернативная версия, которая создает необходимые функции утилиты с нуля.
Это предполагает, что ваш объект изменений будет и / или будет содержать разреженные массивы. Если нет, то как вы планируете сопоставить вещи?
Вот мой подход:
// Helper or utility functions
function * getPaths(o, p = []) {
if (Object(o) !== o || Object .keys (o) .length == 0) yield p
if (Object(o) === o)
for (let k of Object .keys (o))
yield * getPaths (o[k], [... p, Number.isInteger (Number (k)) ? Number (k) : k])
}
const allPaths = (o) => [... getPaths(o)]
// Main function
const applyChanges = (obj, changes) =>
reduce ((o, p) => assocPath (p, path (p, changes), o), obj, allPaths (changes))
// Sample data
const base = [
{a: 1, b: {c: 11, d: [{e: 100}, {e: 111}]}},
{a: 2, b: {c: 22, d: [{e: 200}, {e: 222}]}},
{a: 3, b: {c: 33, d: [{e: 300}, {e: 333}]}},
]
const deltas = [
{a: 8, b: { d: [ , {e: 888}]}},
,
{ b: {c: 99, d: [{e: 999}, ]}},
]
// Demonstration
console .log (
applyChanges (base, deltas)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.0/ramda.js"></script>
<script> const {reduce, assocPath, path} = R </script>
allPaths
находит пути ко всем конечным узлам в объекте, где индексы массива отображаются в виде чисел, а другие ключи в виде строк. Например,
const foo = {a: 42, b: {c: 12, d: [{e: 10}, {e: 20}]}}
allPaths (foo) //=> [["a"], ["b", "c"], ["b", "d", 0, "e"], ["b", "d", 1, "e"]]
Это просто тонкая оболочка вокруг функции генератора getPaths
, которая выполняет рекурсивную тяжелую работу для этого. Мы могли бы написать простую рекурсивную версию этого, но генераторы часто упрощают написание таких обходов.
Имея список путей в объекте изменений, мы можем затем применить значения, чтобы создать новую копию нашего Основной объект. Это сделано в applyChanges
, нашей основной функции. Он находит пути в объекте changes
, а затем использует assocPath
и reduce
Рамды, чтобы сложить их в наш основной объект.
Здесь у нас могут быть некоторые недостатки в скорости и памяти по двум причинам. Для скорости мы ищем значение на каждом пути, когда мы вызываем path(p, changes)
, но мы уже сделали соответствующий обход в getPath
. Вероятно, будет некоторая экономия в отчете о другой структуре с path
и value
из getPath
и последующем использовании их в applyChages
. Это не влияет на сложность алгоритма c, а только на коэффициенты, и я не стал бы беспокоиться об этом, если бы у него не было измеримых проблем. Что касается памяти, этот стиль reduce
с assocPath
включает создание новых объектов на каждой итерации. Учитывая, что существует важное структурное разделение, это может не иметь большого значения, но для большого объекта changes
это может быть проблемой. (Это не будет для меня серьезным беспокойством, но я держу такие вещи в затылке.)
Без Рамды
Поскольку я склонен думать в Рамде, я написал выше с помощью инструментов Ramda. Но есть только несколько функций. В этом случае R.reduce
можно легко заменить на Array.prototype.reduce
, и мы можем довольно легко написать наши собственные версии R.assocPath
и R.path
. Вот еще одна версия, которая не использует библиотеку:
// Utility functions
const isInt = Number.isInteger
const path = (ps = [], obj = {}) =>
ps .reduce ((o, p) => (o || {}) [p], obj)
const assoc = (prop, val, obj) =>
isInt (prop) && Array .isArray (obj)
? [... obj .slice (0, prop), val, ...obj .slice (prop + 1)]
: {...obj, [prop]: val}
const assocPath = ([p = undefined, ...ps], val, obj) =>
p == undefined
? obj
: ps.length == 0
? assoc(p, val, obj)
: assoc(p, assocPath(ps, val, obj[p] || (obj[p] = isInt(ps[0]) ? [] : {})), obj)
// Helper functions
function * getPaths(o, p = []) {
if (Object(o) !== o || Object .keys (o) .length == 0) yield p
if (Object(o) === o)
for (let k of Object .keys (o))
yield * getPaths (o[k], [...p, isInt (Number (k)) ? Number (k) : k])
}
const allPaths = (o) => [... getPaths(o)]
// Main function
const applyChanges = (obj, changes) =>
allPaths(changes).reduce((o, p) => assocPath(p, path(p, changes), o), obj)
// Sample data
const base = [
{a: 1, b: {c: 11, d: [{e: 100}, {e: 111}]}},
{a: 2, b: {c: 22, d: [{e: 200}, {e: 222}]}},
{a: 3, b: {c: 33, d: [{e: 300}, {e: 333}]}},
]
const deltas = [
{a: 8, b: { d: [ , {e: 888}]}},
,
{ b: {c: 99, d: [{e: 999}, ]}},
]
// Demonstration
console .log (
applyChanges (base, deltas)
)
Прямой подход
Обе эти версии используют довольно косвенный подход к проблеме. Мне пригодились эти инструменты, которые позволили мне быстро построить основную функцию. Но я уверен, что есть более прямой рекурсивный подход. Если я найду время, я поищу его.