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'
});