@dmitry-toster

Как найти и заменить значение вложенного объекта?

Есть объект:
const data = {
  key: ['value1', 'value2'],
  find: {
    key: 'key',
    key2: [{
      key12: 'value12',
      key45: 'value 45',
      value: 'some value'
    }, {
      key32: 'value122',
      key33: 'value 435',
      value: 'some value'
    }, {
      key56: 'value56',
      key51: 'value 5111',
      value: 'some value'
    }]  
  },
  key22: ['some', 'test', 'value'],
};

Задача: найти и изменить значение у ключа value
Что у меня получилось:
const newData = {...data, find: {
  ...data.find,
  key2: {
    ...data.find.key2.map(obj => {
      return {...obj, value: 'new value'}
    })
  }
}}

Но проблема в том, что неизвестно какая у объекта будет вложенность и какие будут ключи, но известно точно, что будет свойство value, которое нужно найти и заменить. Как можно решить эту задачу при текущем условии?

UPD: Пока писал вопрос на ум приходит рекурсия, копировать в новый объект все ключи пока не будет найден нужный. А в нужном уже заменять значение. Попробую написать сам, но ваших ответов тоже жду))
  • Вопрос задан
  • 4620 просмотров
Решения вопроса 3
Aetae
@Aetae Куратор тега JavaScript
Тлен
Рекурсивно, вестимо.

Если ещё и нужно клонировать объект не изменяя исходный(сужу по коду), то можно совместить приятное с полезным:
const newData = JSON.parse(
  JSON.stringify(data), 
  (key, value) => key === 'value' ? 'new value' : value
);

P.S. Это сработает, только если объект - преобразуемый в json(не содержит истансов каких-либо классов, функций и т.п.).
Ответ написан
@dmitry-toster Автор вопроса
В виду того, что сериализация и парсинг JSON очень дорогая операция (см. тест производительности), было принято решение написать свою рекурсивную функцию:

function clone(obj) {
    if (obj === null || typeof(obj) !== 'object' || 'isActive' in obj) return obj;

    const temp = obj.constructor();

    for (let key in obj) {
        if (Object.prototype.hasOwnProperty.call(obj, key)) {
            obj['isActive'] = null;
            temp[key] = key === 'value' ? 'new value' : clone(obj[key]);
            delete obj['isActive'];
        }
    }

    return temp;
}

clone(data);

P.s спасибо пользователю Дмитрий Беляев за совет который сподвигнул к изучению данного вопроса
Ответ написан
bingo347
@bingo347 Куратор тега JavaScript
Crazy on performance...
function cloneWithReplace(
    // клонируемый объект
    obj,
    // объект с функциями вида value => newValue, в соответствующих ключах для замены
    // нужен не всегда, поэтому по умолчанию сделаем пустой объект
    replacers = {},
    // Map с значениями, которые уже склонировали, дабы не попасть в рекурсию
    // так как внешний код обычно его не будет передавать, зададим значение по умолчанию
    storeMap = new Map()
) {
    // для начала чекнем, что объект уже клонировали:
    if(storeMap.has(obj)) {
        return storeMap.get(obj);
    }

    // получим тип объекта, он нам пригодится пару раз, дабы отличать функции
    const type = typeof obj;

    // если obj примитив, то его можно просто вернуть
    if(obj === null || (type !== 'object' && type !== 'function')) {
        return obj;
    }

    // создадим переменную с результатом и инициируем ее в зависимости от типа оригинала
    let result;
    if(type === 'function') {
        // функцию можно "склонировать" лишь обернув
        result = function() {
            return obj.apply(this, arguments);
        };
        // неплохо бы, чтоб клон правильно сообщал имя функции и количество аргументов
        // но так как IE не ест такую магию, обернем в try-catch
        try {
            Object.defineProperties(result, {
                name: Object.getOwnPropertyDescriptor(obj, 'name'),
                length: Object.getOwnPropertyDescriptor(obj, 'length')
            });
        } catch {}
    } else if(Array.isArray(obj)) {
        // массивы клонируем рекурсивно, при помощи map
        result = obj.map(value => cloneWithReplace(value, replacers, storeMap));
        // так как нормальные массивы не содержат других полей, кроме числовых
        // можно сохранить клон в защиту от рекурсии и вернуть результат
        storeMap.set(obj, result);
        return result;
    } else {
        // для всех других объектов просто создадим новый объект и скопируем ему ссылку на прототип
        result = Object.setPrototypeOf({}, Object.getPrototypeOf(obj));
    }

    // сохраним клон в защиту от рекурсии
    storeMap.set(obj, result);

    // осталось склонировать поля с заменой тех случаев, где у нас есть replacer
    for(const key of Object.getOwnPropertyNames(obj).concat(Object.getOwnPropertySymbols(obj))) {
        if(typeof replacers[key] === 'function') {
            // если есть replacer используем его
            result[key] = replacers[key](obj[key]);
        } else {
            // иначе клонируем поле рекурсивно
            result[key] = cloneWithReplace(obj[key], replacers, storeMap);
        }
    }

    return result;
}

// используем так:
const newData = cloneWithReplace(data, {
    value: () => 'new value'
});
Ответ написан
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы