/**
* Deep-clones an Object using JSON.parse() and JSON.stringify(). This means
* that complex properties, such as functions, will not be copied. Use this
* function therefore to copy objects that contain literal values or arrays
* and nested objects of them.
*
* @param {Object} obj The Object to deep-clone
* @returns {Object} A copy of the given object.
*/
const deepCloneObject = obj => JSON.parse(JSON.stringify(obj));
/**
* Merges two or more objects and returns a new object. Recursively merges
* nested objects. Iterates the objects in the order they were given. If a
* key is present in the next object as well, it will overwrite the value
* of the previous object. Atomic properties and arrays are replaced in
* the resulting object. Passing two objects with the first being an empty
* new object, you may use this function to clone objects as well. Object-
* properties that are actually Class-instances will be copied (i.e. it
* will point to the same instance in the merged object).
*
* @param {...Object} objects Two or more Objects to merge. If only one
* Object is passed, it is returned as-is.
* @throws {Error} If no object is passed, needs one or more.
* @returns {Object} The result of the merge. The resulting Object will be
* created without a prototype.
*/
const mergeObjects = (...objects) => {
if (objects.length === 0) {
throw new Error('No objects were given.');
} else if (objects.length === 1) {
return objects[0];
}
const target = Object.create(null);
objects.reduce((prev, next) => {
// Object.keys() does not enumerate __proto__.
for (const key of Object.keys(next)) {
const nextType = Object.prototype.toString.call(next[key]);
switch (nextType) {
case '[object Null]':
case '[object Number]':
case '[object String]':
case '[object Boolean]':
case '[object Function]':
case '[object AsyncFunction]':
case '[object Undefined]':
case '[object RegExp]':
prev[key] = next[key];
break;
case '[object Array]':
// While the contents stay the same, the array itself is not copied.
prev[key] = Array.prototype.slice.call(next[key], 0);
break;
case '[object Object]':
// Check if the current value is a class instance (ctor different from Object):
try {
const proto = Object.getPrototypeOf(next[key]);
if (proto === null || proto.constructor !== Object) {
prev[key] = next[key];
break;
}
} catch (e) { } // just move on
if (Object.prototype.toString.call(prev[key]) !== '[object Object]') {
prev[key] = {};
}
prev[key] = mergeObjects(prev[key], next[key]);
break;
default:
// Unfortunately, nodejs does not yet support /\[object\s\p{Letter}[a-z0-9_\-]*\]/iu
if (/\[object\s[a-z_\-][a-z0-9_\-]*\]/i.test(nextType)) {
// This will match all other built-in types.
prev[key] = next[key];
}
continue;
}
}
return prev;
}, target);
return target;
};
module.exports = Object.freeze({
deepCloneObject,
mergeObjects
});