// Import *most* required bits here.
const {N,F, isObj, isComplex, def, isArray, console} = require('../types');
const copyProps = require('./copyprops');
const getProp = require('./getproperty');
/**
* Clone an object or function.
*
* @param {object} obj - The object we want to clone.
*
* @param {(object|number)} [opts={}] Options for the cloning process.
*
* If this is a `number` then it's assumed to be the `opts.mode` parameter.
*
* @param {number} [opts.mode=CLONE.DEF] One of the `CLONE.*` enum values.
*
* When the `clone()` method was written to replace some similar methods
* from earlier libraries, I for some reason decided to simply have a bunch
* of different cloning modes. I have since added a full set of options that
* allows overriding the options of any mode (except `CLONE.JSON`).
*
* The `CLONE` enum is also aliased as `clone.MODE` as an alternative.
*
* @param {boolean} [opts.all] Clone **all** of the object's properties?
*
* If `false` only *enumerable* properties will be cloned.
*
* The default value depends on the `opts.mode` used.
*
* This is not used if `opts.mode` was `CLONE.JSON`.
*
* @param {boolean} [opts.slice] Use the `Array.slice()` shortcut?
*
* If `true` then when cloning `Array` objects, a shallow clone will be
* created using the `Array.slice()` method.
*
* The default value depends on the `opts.mode` used.
*
* This is not used if `opts.mode` was `CLONE.JSON`.
*
* @param {boolean} [opts.recursive] Clone nested objects recursively?
*
* The default value depends on the `opts.mode` used.
* If `opts.slice` is also `true` then `Array` objects will
* be *shallow clones* while any other kind of object will be recursive.
*
* This is not used if `opts.mode` was `CLONE.JSON`.
*
* @param {boolean} [opts.descriptors] Clone using the property descriptors?
*
* If `true` we will get the property descriptors from the original object,
* and assign them to the clone.
* This is the only way to clone *accessor* type properties properly.
*
* If `false` we will directly assign the *property value* from the original
* object into the clone. This means the *current value* returned from an
* *accessor* type property will be assigned statically to the clone.
*
* The default value will be `true` if `opts.all` **OR** `opts.recursive`
* are `true`. It will be `false` otherwise.
*
* This is not used if `opts.mode` was `CLONE.JSON`.
*
* @param {boolean} [opts.prototype] Set the clone's `prototype`?
*
* If `true`, once the cloning is complete, we will call
* `Object.getPrototypeOf()` on the original `obj`, and then call
* `Object.setProrotypeOf()` on the clone.
*
* If `false` then the clone with have a prototype of `Object` or
* `Array` depending on whether the original object was an `Array`
* or not. No further prototype handling will be done.
*
* The default value will be `true` if `opts.all` **AND** `opts.recursive`
* are *both* `true`. Otherwise the default value is `false`.
* So for *modes*, only `CLONE.ENTIRE` uses this by default.
*
* @param {(object|boolean)} [opts.addClone=false]
* Call `addClone()` on the cloned object.
*
* If `opts.addClone` is an `object` then it will be used as the options
* passed to the `addClone()` method. If it is `true` then the `opts`
* will be passed as is.
*
* @param {(object|boolean)} [opts.addLock=false]
* Call `addLock()` on the cloned object.
*
* If `opts.addLock` is an `object` then it will be used as the options
* passed to the `addLock()` method. If it is `true` then the `opts`
* will be passed as is.
*
* @param {(object|boolean)} [opts.copy=false]
* Call `copyProps()` on the cloned object.
*
* This is called *after* the normal cloning process, so only properties
* that weren't copied during the cloning process will be copied here.
* This is a leftover from when `CLONE.JSON` was the default cloning mode,
* and this was the only way to restore `function` properties.
*
* If `opts.copy` is an `object` then it will be used as the options
* passed to the `copyProps()` method. If it is `true` then the `opts`
* will be passed as is.
*
* @return {object} The clone of the object.
*
* @alias module:@lumjs/core/obj.clone
*/
function clone(obj, opts={})
{
//console.debug("clone()", obj, opts);
if (!isComplex(obj))
{ // Doesn't need cloning.
//console.debug("no cloning required");
return obj;
}
if (typeof opts === N)
{ // The 'mode' option.
opts = {mode: opts};
}
else if (!isObj(opts))
{ // Opts has to be a valid object.
opts = {};
}
// The mode is the base option.
const mode = typeof opts.mode === N ? opts.mode : CLONE.DEF;
// Options with defaults based on the mode.
const allProps = opts.all ?? ALL_PROPS.includes(mode);
const useSlice = opts.slice ?? SLICE_ARRAYS.includes(mode);
const recursive = opts.recursive ?? RECURSIVE.includes(mode);
// Some that depend on the values of the above options.
const descriptors = opts.descriptors ?? (allProps || recursive);
const withProto = opts.prototype ?? (allProps && recursive);
// Finally, a few that can be boolean or objects.
const subOpts = opt =>
{
if (isObj(opts[opt]))
return opts[opt];
else if (opts[opt] === true)
return opts;
}
const reclone = subOpts('addClone');
const relock = subOpts('addLock');
const cpProps = subOpts('copy');
let copy;
//console.debug("::clone", {mode, reclone, relock});
if (mode === CLONE.JSON)
{ // Deep clone enumerable properties using JSON trickery.
//console.debug("::clone using JSON cloning");
copy = JSON.parse(JSON.stringify(obj));
}
else if (Array.isArray(obj) && useSlice)
{ // Make a shallow copy using slice.
//console.debug("::clone using Array.slice()");
copy = obj.slice();
}
else
{ // Build a clone using a simple loop.
//console.debug("::clone using simple loop");
copy = isArray(obj) ? [] : {};
let props;
if (allProps)
{ // All object properties.
//console.debug("::clone getting all properties");
props = Object.getOwnPropertyNames(obj);
}
else
{ // Enumerable properties.
//console.debug("::clone getting enumerable properties");
props = Object.keys(obj);
}
//console.debug("::clone[props]", props);
let recOpts;
if (recursive)
{ // Recursive opts; skips addClone, addLock, and copy.
recOpts =
{
mode, recursive, descriptors,
all: allProps,
slice: useSlice,
prototype: withProto,
}
}
for (const prop of props)
{
if (descriptors)
{ // Use descriptor assignment.
const objDesc = getProp(obj, prop);
if (isObj(objDesc))
{ // Make a fast, shallow clone of the descriptor.
const cloneDesc = clone(objDesc);
if (isObj(objDesc.value) && recursive)
{ // Recursively clone the value.
cloneDesc.value = clone(objDesc.value, recOpts);
}
def(copy, prop, cloneDesc);
}
else
{
console.error("getProperty failed", {obj, prop, props, opts});
}
}
else
{ // Use direct assignment.
let val = obj[prop];
if (isObj(val) && recursive)
{ // Deep cloning.
val = clone(val, recOpts);
}
copy[prop] = val;
}
} // for prop
} // simple loop cloning algorithm
if (withProto)
{ // Copy the prototype if it is different.
const objProto = Object.getPrototypeOf(obj);
const copyProto = Object.getPrototypeOf(copy);
if (objProto && objProto !== copyProto)
{ // Update the clone's prototype to match the original.
Object.setPrototypeOf(copy, objProto);
}
}
if (isObj(reclone))
{ // Add the clone() method to the clone, with the passed opts as defaults.
addClone(copy, reclone);
}
if (isObj(relock))
{ // Add the lock() method to the clone.
addLock(copy, relock);
}
if (isObj(cpProps))
{ // Pass the clone through the copyProps() function as well.
copyProps(obj, copy, cpProps);
}
return copy;
}
// Export the clone here.
exports.clone = clone;
/**
* Add a clone() method to an object.
*
* @param {object|function} obj - The object to add clone() to.
* @param {object} [defOpts=null] Default options for the clone() method.
*
* If `null` or anything other than an object, the defaults will be:
*
* ```{mode: CLONE.DEF, addClone: true, addLock: false}```
*
* @alias module:@lumjs/core/obj.addClone
*/
function addClone(obj, defOpts=null)
{
if (!isObj(defOpts))
{ // Assign a default set of defaults.
defOpts = {addClone: true};
}
const defDesc = defOpts.cloneDesc ?? {};
defDesc.value = function (opts)
{
if (!isObj(opts))
opts = defOpts;
return clone(obj, opts);
}
return def(obj, 'clone', defDesc);
}
exports.addClone = addClone;
/**
* Clone an object if it's not extensible (locked, sealed, frozen, etc.)
*
* If the object is extensible, it's returned as is.
*
* If not, if the object has a `clone()` method it will be used.
* Otherwise use our `clone()` function.
*
* @param {object} obj - The object to clone if needed.
* @param {object} [opts] - Options to pass to `clone()` method.
*
* @return {object} - Either the original object, or an extensible clone.
*
* @alias module:@lumjs/core/obj.cloneIfLocked
*/
function cloneIfLocked(obj, opts)
{
if (!Object.isExtensible(obj))
{
if (typeof obj.clone === F)
{ // Use the object's clone() method.
return obj.clone(opts);
}
else
{ // Use our own clone method.
return clone(obj, opts);
}
}
// Return the object itself, it's fine.
return obj;
}
exports.cloneIfLocked = cloneIfLocked;
// Import `addLock()` here *after* assigning the clone methods.
const {addLock} = require('./lock');
// And setting all the Enum definitions at the very end.
const Enum = require('../enum');
/**
* An `enum` of supported *modes* for the `clone()` method.
*
* - **P** → All properties. If unchecked, *enumerable* properties only.
* - **A** → Uses `Array.slice()` shortcut for shallow `Array` cloning.
* - **R** → Recursive (deep) cloning of nested objects.
* - **D** → Uses *descriptor* cloning instead of direct assignment.
* - **T** → Sets the `prototype` of the clone as well.
*
* | Mode | P | A | R | D | T | Notes |
* | ---- | - | - | - | - | - | ----- |
* | `CLONE.N` | × | × | × | × | × | Can be used to manually specify options. |
* | `CLONE.DEF` | × | ✓ | × | × | × | Default mode for cloning functions. |
* | `CLONE.DEEP` | × | × | ✓ | ✓ | × | |
* | `CLONE.FULL` | ✓ | ✓ | × | ✓ | × | |
* | `CLONE.ALL` | ✓ | × | × | ✓ | × | |
* | `CLONE.ENTIRE` | ✓ | × | ✓ | ✓ | ✓ | |
* | `CLONE.JSON` | - | - | ✓ | - | × | Uses JSON, so no `function` or `symbol` support. |
*
* The `✓` and `×` marks signify the *default settings* in the mode.
* In *most* cases there are options that can override the
* defaults.
*
* Any feature in the `CLONE.JSON` row marked with `-` are
* incompatible with that mode and cannot be enabled at all.
*
* @alias module:@lumjs/core/obj.CLONE
*/
const CLONE = Enum(['N','DEF','FULL','ALL','DEEP','ENTIRE','JSON']);
exports.CLONE = CLONE;
// A list of modes that should use the array.slice shallow shortcut.
const SLICE_ARRAYS = [CLONE.DEF, CLONE.FULL];
// A list of modes that should get *all* properties.
const ALL_PROPS = [CLONE.FULL, CLONE.ALL, CLONE.ENTIRE];
// A list of modes that should do recursive cloning of objects.
const RECURSIVE = [CLONE.DEEP, CLONE.ENTIRE];
// Alias the CLONE enum as clone.MODE
def(clone, 'MODE', CLONE);