Обновить объект Javascript, вложенные данные — вставить, только если они не подлежат обновлению

#javascript #arrays #object #insert #lodash

#javascript #массивы #объект #вставить #Lodash

Вопрос:

Допустим, у меня есть следующие данные

 var obj = {
  test: 'somedata',
  scores: [
    {
      "points":99, 
      "id":"x12"
    },
    {
      "points":21, 
      "id":"x13"
    }
  ],
  sites: [
    {
      "exercises":21, 
      "sid":"s12"
    },
    {
      "exercises":23, 
      "sid":"s11"
    }
  ],
  history: {
    key: 'value',
    commits: [
      {
         id: 1,
         value: 'thank you'
      }
    ]
  }
}
 

Обратите внимание, что scores и sites содержат массивы с уникальными элементами на основе id in scores и на основе sid in sites . Я хочу функцию, которая выполняет следующую магию:

 //will **update** obj.test to 'newdata' and return {test:'newdata'}
magicUpdate(obj, {test:'newdata'}) 

//will **insert** obj.newkey with with value 'value' and return {newkey: 'value'}
magicUpdate(obj, {newkey: 'value'})

//will do nothing and return {}
magicUpdate(obj, {scores: []})

//will **update** scores[0] and return {scores:[{points:3, id: "x12"}]}, as id "x12" is already in the array at index 0
magicUpdate(obj, {scores:[{points:3, id: "x12"}])

//will **insert** {points:3, id: "x14"} into obj.scores and return {scores:[{points:3, id: "x14"}]}
magicUpdate(obj, {scores:[{points:3, id: "x14"}]})

//will **update** sites[0] and return {sites:[{exercises:22, sid: "s12"}]}, as id "s12" is already in the array at index 0
magicUpdate(obj, {sites:[{exercises:22, sid: "s12"}])

//will **insert** {exercises:10, sid: "s14"} into obj.sites and return {sites:[{exercises:10, sid: "s14"}]}
magicUpdate(obj, {sites:[{exercises:10, sid: "s14"}]})

//and also be recursive ...
//will **update** obj.history.commits[0]
magicUpdate(obj, {'history.commits': [{id:1, value: 'changed'}]});
 

Я видел .update, выполняющий рекурсию, но только в том случае, если кто-то передает path , который должен определяться автоматически. Затем есть .merge, который внутренне использует _.baseMerge и очень близок к тому, что мне нужно, хотя я не понимаю сигнатуру функции.

 _.merge(
{scores:[{id: 12, points:10}, {id: 13, points:10}]}, 
{scores:[{id: 14, points:10}, {id: 15, points:10}]}
) 
// returns {scores:[{id: 14, points:10}, {id: 15, points:10}]} not the fully merged array
 

Может кто-нибудь указать мне хорошее направление или добиться аналогичных результатов с помощью Lodash?

Комментарии:

1. И что вы пробовали до сих пор?

2. Я попытался написать свою собственную функцию, которая закончилась неудачно, когда я заметил, что мне также нужна рекурсия. Затем я нашел функции Lodash выше и подумал, что у кого-то, возможно, была такая же проблема раньше.

3. Я заметил, что слияние отлично работает для объектов и вложенных объектов. Может быть, я просто пытаюсь преобразовать свою структуру данных из scores : [{id: s1, points:2},...] в scores:{s1:{points:2},...}

4. https://jsfiddle.net/r0507fyh/

Ответ №1:

magicUpdate Функция, которую вы упоминаете в своем сообщении, действительно может быть достигнута с использованием функций Lodash.

Для этой реализации я использовал в основном _ .get , _ .set и _ .UnionWith, хотя я уверен, что это могло быть достигнуто с помощью некоторых других:

 // src will be mutated. For simplicity's sake, obj is an object with only one property that represents the changes to make to src
function magicUpdate(src, obj) {
  var key = _.first(_.keys(obj)),
    value = _.get(obj, key),
    srcValue = _.get(src, key),
    comparator = function(a, b) {
      var idKey = _.isUndefined(a.id) ? 'sid' : 'id';
      return a[idKey] === b[idKey];
    }

  if (_.isArray(srcValue)) {
    value = _.unionWith(value, srcValue, comparator);
  }

  return _.set(src, key, value);
}
 

Как вы, возможно, заметили, просматривая код, возвращаемый тип — это измененный объект, а не то, что вы спрашиваете. Я не был уверен, что вы хотите в качестве возвращаемого значения.

В любом случае, в Lodash нет встроенной функции разности объектов, поэтому было бы необходимо разработать что-то подобное на случай, если вам нужна разница между старым объектом и измененным (вам также нужно _ .clone объект сначала, чтобы иметь копию и иметь возможность сравнивать).

Идея функции, которую я представляю, состоит в том, чтобы попытаться получить ключ obj (это ключ, который мы хотим изменить src ) и проверить, существует ли он и является ли он массивом. Если это так, мы просто добавляем два массива, обновляя те, в src которых есть одинаковые id obj . Из-за того, что sites , scores и history пришлось id , и sid мне пришлось добавить еще немного логики в компаратор _.unionWith функции.

Если key не существует или не является массивом, мы просто устанавливаем его src .

Здесь у вас есть скрипка на случай, если вы захотите поиграть с ней. Надеюсь, это поможет.

Обновить

Мое первое решение предназначалось для одного свойства, обновляемого одновременно. Однако, похоже, что одновременно можно обновлять более одного.

Одним из быстрых решений может быть перебор объекта с обновлениями и обновление по одному свойству за раз.

 function updateProperty(src, obj) {
  var key = _.first(_.keys(obj)),
    value = _.get(obj, key),
    srcValue = _.get(src, key),
    comparator = function(a, b) {
      var idKey = _.isUndefined(a.id) ? 'sid' : 'id';
      return a[idKey] === b[idKey];
    }

  if (_.isArray(srcValue)) {
    value = _.unionWith(value, srcValue, comparator);
  }

  return _.set(src, key, value);
}

function magicUpdate(obj, src) {
  _.forEach(src, function(value, key) {
      updateProperty(obj, _.pick(src, key));
  });
  return obj;
}
 

Скрипка

Комментарии:

1. Большое вам спасибо за вашу любезную помощь. К сожалению, это не работает, когда кто-то пытается установить более одного значения одновременно (что, я должен признать, не было рассмотрено в моих примерах выше, но теперь есть). Смотрите здесь: https://jsfiddle.net/qyf3hbe1 / . Я пришел к следующему решению: https://jsfiddle.net/r0507fyh /

2. @niklas Спасибо! Я обновил ответ возможным решением проблемы: используя текущий метод, обновляйте по одному свойству за раз. Это может сработать! Надеюсь, это поможет.

3. Большое вам спасибо. Я обнаружил, что производительность (из-за множества циклов вашего решения) в ~ 10 раз медленнее, чем у пользовательской функции, которую я запрограммировал. Смотрите здесь jsfiddle.net/r0507fyh/3

Ответ №2:

Я написал решение, которое является рекурсивным и довольно производительным. Посмотрите на эту скрипку.

 function mergeRecursive(obj1, obj2) {
  if (obj1.constructor == Array) {
    for (var i = 0; i < obj1.length; i  ) {
      if (obj1[i].id == obj2.id) {
        obj1[i] = obj2;
        return obj1;
      }
    }
    obj1.push(obj2);
    return obj1;
  }
  for (var p in obj2) {
    // Property in destination object set; update its value.
    if (obj2[p].constructor == Array) {
      obj2[p].forEach(function(arrayElement) {
        obj1[p] = MergeRecursive(obj1[p], arrayElement);
      });
    } else if (obj2[p].constructor == Object) {
      obj1[p] = MergeRecursive(obj1[p], obj2[p]);
    } else {
      obj1[p] = obj2[p];
    }
  }
  return obj1;
}