Рекурсивный обход массива и изменение значений свойств объекта в JavaScript

#javascript #node.js #algorithm #recursion #data-structures

#javascript #node.js #алгоритм #рекурсия #структуры данных

Вопрос:

Сообщение может показаться длинным, но его довольно легко прочитать, если нет, я добавлю больше деталей

У меня есть criteria массив, который выглядит следующим образом :

    let criteria = [
        "and",
        {
          "Collection": "persons",
          "Property": "phone",
          "operator": "eq",
          "operatorValue": "$p:phone"  
        },
        {
          "Collection": "persondetails",
          "Property": "country",
          "operator": "eq",
          "operatorValue": "$p:country" 
        },
        ["or",
        {
          "Collection": "persons",
          "Property": "city",
          "operator": "eq",
          "operatorValue": "$p:city"  
        }]
      ]
  

Характеристики criteria :

  1. В нем могут быть вложенные массивы.
  2. Массив первого элемента (или вложенный массив) всегда будет либо «и», либо «или»
  3. Второй элемент в массиве, элемент может быть либо объектом с этой конкретной структурой

{ "Collection": "persons", "Property": "phone", "operator": "eq", "operatorValue": "$p:phone" }

или это может быть массив, подобный:

["or", { "Collection": "persons", "Property": "city", "operator": "eq", "operatorValue": "$p:city" }]

  1. Объект никогда не будет вложенным объектом

Также существует parameters объект:

 let parameters = {phone:"23138213", "country": "Russia", "city":"york"}
  

Цель состоит в том, чтобы рекурсивно пройти через все operatorValue свойства в criteria массиве, и если вы встретите такое значение, как $p:phone , оно должно быть заменено любым parameters["phone"] значением, равным.

ОЖИДАЕМЫЙ РЕЗУЛЬТАТ:

 [
    "and",
    {
      "Collection": "persons",
      "Property": "phone",
      "operator": "eq",
      "operatorValue": "23138213"  
    },
    {
      "Collection": "persondetails",
      "Property": "country",
      "operator": "eq",
      "operatorValue": "Russia" 
    },
    ["or",
    {
      "Collection": "persons",
      "Property": "city",
      "operator": "eq",
      "operatorValue": "york"  
    }]
  ]
  

Я смог рекурсивно перемещаться по массиву. Единственная проблема в том, что я не могу понять, как изменить исходную criteria переменную.

REPL

Пожалуйста, смотрите строку 43 в repl. item[1]=parameters[item[1].split('$p:')[1]] Я понимаю, почему это не изменит критерии, потому что элемент здесь — это совсем другая переменная в другой области видимости.

Неудачная попытка :

   function traverse(obj,parameters){
  
    obj.forEach((item,index)=>{
      
      
     if( typeof item == 'string' ){
       //do nothing
     }
     else if( !(item instanceof Array)){
         
       Object.entries(item).forEach((item,index)=>{
         
         if( item[1] instanceof Array){ 
                      
           traverse(item,parameters);
         }else{
           if(item[1].startsWith('$p:')){
               item[1]=parameters[item[1].split('$p:')[1]] //values dont get replaced for obvious reason
               console.log(item[1])
           } 
         }
       })        
     }
     else if( item  instanceof Array){           
           traverse(item,parameters);
      }  
    })
  }

  traverse(criteria,parameters)
  console.log(criteria)
  

Как мне решить эту проблему?

Ответ №1:

Вы могли бы упростить свою функцию. Вам не нужно перебирать записи объекта. Вам также не нужно split operationValue . Этот ключ отображения parameters присутствует в Property ключе.

  • Перебирайте каждый элемент в массиве и проверяйте, является ли элемент Array .
  • Если да, выполните рекурсивный вызов traverse для элемента.
  • Если это объект, обновите его operatorValue свойство с помощью parameters[val.Property]

 function traverse(arr, parameters) {
  for (const item of arr) {
    if (Array.isArray(item))
        traverse(item, parameters)
    else if (typeof item === 'object')
        item.operatorValue = parameters[item.Property]
  }
  return arr
}

let criteria=["and",{Collection:"persons",Property:"phone",operator:"eq",operatorValue:"$p:phone"},{Collection:"persondetails",Property:"country",operator:"eq",operatorValue:"$p:country"},["or",{Collection:"persons",Property:"city",operator:"eq",operatorValue:"$p:city"}]],
    parameters = {phone:"23138213", "country": "Russia", "city":"york"};

console.log(traverse(criteria, parameters))  

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

1. Интересно, почему это работает, а мое решение — нет. Это потому, что obj.forEach((item,index)=>{} создается новый, item но for (const item of arr) {} этого не делает?

2. @SamuraiJack Вам нужно переименовать Object.entries(item).forEach((item) <- item paraeter во что-то другое. Потому что снаружи есть item переменная. item[1]=parameters[item[1].split('$p:')[1]] просто собирается обновить массив записей. Он не обновит внешний item объект, который вы хотите обновить

3. Вашей логике трудно следовать, но item[1]=parameters[item[1].split('$p:')[1]] просто переназначает второй элемент из Object.entries , который является простой строкой внутри массива. Это не меняет исходный объект — вам нужна ссылка на объект, чтобы изменить его свойство.

4. @SamuraiJack кроме того, вам не нужно перебирать записи объекта. Вы знаете свойства заранее. split Также не является необходимым, поскольку Property ключ каждого объекта уже содержит ключ, из parameters которого вы хотите взять

5. Спасибо @adiga

Ответ №2:

Вам нужно взять ключ объекта для присвоения значения.

 function traverse(obj, parameters) {
    obj.forEach(item => {
        if (item instanceof Array) return traverse(item, parameters);
        if (item?.operatorValue?.startsWith('$p:')) {
            item.operatorValue = parameters[item.operatorValue.split('$p:')[1]];
        }
    });
}

const
    criteria = ["and", { Collection: "persons", Property: "phone", operator: "eq", operatorValue: "$p:phone" }, { Collection: "persondetails", Property: "country", operator: "eq", operatorValue: "$p:country" }, ["or", { Collection: "persons", Property: "city", operator: "eq", operatorValue: "$p:city" }]],
    parameters = { phone: "23138213", "country": "Russia", "city": "york" };

traverse(criteria, parameters);
console.log(criteria);  
 .as-console-wrapper { max-height: 100% !important; top: 0; }  

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

1. Почему это решение работает, а мое нет?

2. вы присваиваете массиву значение объекта. он не имеет ссылки на объект, принимая записи объекта. чтобы преодолеть это, вам нужен объект и ключ и присвоить ему новое значение.

3. Спасибо, Нина. Это была такая глупая ошибка с моей стороны. Я отметил ответ адиги, потому что он был первым. 🙂 1

4. @SamuraiJack Вы должны выбрать лучший ответ, а не первый ответ.

5. @ggorlen и что вы делаете, когда они оба одинаково хороши?

Ответ №3:

Чистая рекурсивная версия:

 const traverse = ([conj, ...nodes], params) => [
  conj, 
  ... nodes .map (node => Array .isArray (node) 
        ? traverse (node, params)
        : {... node, operatorValue: params [nodes .Property]}
      )
]

const criteria = ["and", {Collection: "persons", Property: "phone", operator: "eq",operatorValue: "$p:phone"}, {Collection: "persondetails", Property: "country", operator: "eq", operatorValue: "$p:country"}, ["or", {Collection: "persons", Property: "city", operator: "eq", operatorValue: "$p:city"}]]
const parameters = {phone:"23138213", "country": "Russia", "city":"york"}


console .log (traverse (criteria, parameters))  
 .as-console-wrapper {min-height: 100% !important; top: 0}  

Это работает только в том случае, если упрощение из adiga подтверждает, что Property узел объекта имеет то же значение, что и суффикс в operatorValue единице. Если нет, я бы, вероятно, использовал замену регулярного выражения для обработки этого и разделил его на вспомогательную функцию:

 const replaceOpValue = (params) => (obj) => ({
  ... obj, 
  operatorValue: obj .operatorValue .replace (/$p:(. )/, ((_, key) => params [key]))
})
  

const traverse = ([conj, ...nodes], params) => [
  conj, 
  ... nodes .map (node => Array .isArray (node) 
        ? traverse (node, params)
        : replaceOpValue (params) (node)
      )
]
  

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

1. Только что увидел ветку комментариев, в которой объяснялось, что значение не всегда будет заменено. Вторая версия здесь все еще будет работать. Однако это не обрабатывает случай, когда искомое свойство отсутствует в объекте parameters. Я предполагаю, что тогда по умолчанию потребуется вернуться к исходному значению с помощью чего-то вроде: (_, key) => params [key] || obj .operatorValue или более сложной версии того же самого, если допустимые значения параметров включают значения false-y.

Ответ №4:

Вот решение, использующее object-scan. Может быть немного более гибким и поддерживаемым в зависимости от ваших требований

 // const objectScan = require('object-scan');

const criteria = [ 'and', { Collection: 'persons', Property: 'phone', operator: 'eq', operatorValue: '$p:phone' }, { Collection: 'persondetails', Property: 'country', operator: 'eq', operatorValue: '$p:country' }, [ 'or', { Collection: 'persons', Property: 'city', operator: 'eq', operatorValue: '$p:city' } ] ];

const substitute = (obj, params) => objectScan(['**.operatorValue'], {
  rtn: 'count',
  filterFn: ({ value, parent, property }) => {
    if (value.startsWith('$p:') amp;amp; value.slice(3) in params) {
      parent[property] = params[value.slice(3)];
      return true;
    }
    return false;
  }
})(obj);

console.log(substitute(criteria, { phone: '23138213', country: 'Russia', city: 'york' })); // returns number of substitutions
// => 3

console.log(criteria);
// => [ 'and', { Collection: 'persons', Property: 'phone', operator: 'eq', operatorValue: '23138213' }, { Collection: 'persondetails', Property: 'country', operator: 'eq', operatorValue: 'Russia' }, [ 'or', { Collection: 'persons', Property: 'city', operator: 'eq', operatorValue: 'york' } ] ]  
 .as-console-wrapper {max-height: 100% !important; top: 0}  
 <script src="https://bundle.run/object-scan@13.8.0"></script>  

Отказ от ответственности: Я автор object-scan