initial commit
This commit is contained in:
+23
@@ -0,0 +1,23 @@
|
||||
export type Options<T> = {
|
||||
props?: (keyof T)[];
|
||||
nonenumerable?: boolean;
|
||||
};
|
||||
/**
|
||||
* Copy (clone) an object and all its props recursively to get rid of any prop referenced of the
|
||||
* original object. Arrays and the objects inside them are cloned as well.
|
||||
*
|
||||
* Nesting depth is limited only by available memory, and circular references are reproduced in the
|
||||
* copy rather than followed forever. Two props pointing at the same object still point at one
|
||||
* shared copy afterwards.
|
||||
*
|
||||
* @param target Target can be anything
|
||||
* @param [options={}] See type {@link Options} for more details.
|
||||
*
|
||||
* - `{ props: ['key1'] }` will only copy the `key1` property. When using this you will need to cast
|
||||
* the return type manually (in order to keep the TS implementation in here simple I didn't
|
||||
* built a complex auto resolved type for those few cases people want to use this option)
|
||||
* - `{ nonenumerable: true }` will copy all non-enumerable properties. Default is `{}`
|
||||
*
|
||||
* @returns The target with replaced values
|
||||
*/
|
||||
export declare function copy<T>(target: T, options?: Options<T>): T;
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Assigns a prop the copy is keeping, preserving whether it was enumerable. Callers decide *whether*
|
||||
* to keep a non-enumerable prop before getting here, so by this point the answer is always yes.
|
||||
*/
|
||||
function assignProp(carry, key, newVal, originalObject) {
|
||||
if (Object.prototype.propertyIsEnumerable.call(originalObject, key)) {
|
||||
carry[key] = newVal;
|
||||
return;
|
||||
}
|
||||
Object.defineProperty(carry, key, {
|
||||
value: newVal,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Whether `value` is an object literal, as opposed to a class instance, a built-in like `Date`, an
|
||||
* `arguments` object, or an object with a null prototype.
|
||||
*
|
||||
* The prototype check comes first because it is cheap and rejects almost everything; the tag check
|
||||
* is only reached by values that are about to be cloned anyway.
|
||||
*/
|
||||
function isPlainObject(value) {
|
||||
return (Object.getPrototypeOf(value) === Object.prototype &&
|
||||
Object.prototype.toString.call(value) === '[object Object]');
|
||||
}
|
||||
/**
|
||||
* Returns the clone that `value` should be replaced with, and queues it to be filled in later.
|
||||
* Anything that isn't an array or a plain object is returned untouched.
|
||||
*
|
||||
* Reusing the clone already registered in `clones` is what makes circular and shared references
|
||||
* work: the placeholder is registered before its contents exist, so a reference back to an ancestor
|
||||
* resolves to it rather than walking forever.
|
||||
*/
|
||||
function cloneRef(value, clones, sources, dests) {
|
||||
if (typeof value !== 'object' || value === null)
|
||||
return value;
|
||||
const array = Array.isArray(value);
|
||||
if (!array && !isPlainObject(value))
|
||||
return value;
|
||||
const existing = clones.get(value);
|
||||
if (existing !== undefined)
|
||||
return existing;
|
||||
const clone = array ? new Array(value.length) : {};
|
||||
clones.set(value, clone);
|
||||
sources.push(value);
|
||||
dests.push(clone);
|
||||
return clone;
|
||||
}
|
||||
/**
|
||||
* Copy (clone) an object and all its props recursively to get rid of any prop referenced of the
|
||||
* original object. Arrays and the objects inside them are cloned as well.
|
||||
*
|
||||
* Nesting depth is limited only by available memory, and circular references are reproduced in the
|
||||
* copy rather than followed forever. Two props pointing at the same object still point at one
|
||||
* shared copy afterwards.
|
||||
*
|
||||
* @param target Target can be anything
|
||||
* @param [options={}] See type {@link Options} for more details.
|
||||
*
|
||||
* - `{ props: ['key1'] }` will only copy the `key1` property. When using this you will need to cast
|
||||
* the return type manually (in order to keep the TS implementation in here simple I didn't
|
||||
* built a complex auto resolved type for those few cases people want to use this option)
|
||||
* - `{ nonenumerable: true }` will copy all non-enumerable properties. Default is `{}`
|
||||
*
|
||||
* @returns The target with replaced values
|
||||
*/
|
||||
export function copy(target, options = {}) {
|
||||
if (typeof target !== 'object' || target === null)
|
||||
return target;
|
||||
const clones = new Map();
|
||||
// An explicit work list rather than recursion, so nesting depth is bounded by the heap instead
|
||||
// of the call stack. Deeply nested input used to throw a RangeError here.
|
||||
const sources = [];
|
||||
const dests = [];
|
||||
const result = cloneRef(target, clones, sources, dests);
|
||||
const onlyProps = Array.isArray(options.props) ? options.props : undefined;
|
||||
const nonenumerable = options.nonenumerable === true;
|
||||
while (sources.length) {
|
||||
const source = sources.pop();
|
||||
const dest = dests.pop();
|
||||
if (Array.isArray(source)) {
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
if (i in source)
|
||||
dest[i] = cloneRef(source[i], clones, sources, dests);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (nonenumerable) {
|
||||
for (const key of Object.getOwnPropertyNames(source)) {
|
||||
// Skip __proto__ properties to prevent prototype pollution
|
||||
if (key === '__proto__')
|
||||
continue;
|
||||
if (onlyProps && !onlyProps.includes(key))
|
||||
continue;
|
||||
const newVal = cloneRef(source[key], clones, sources, dests);
|
||||
assignProp(dest, key, newVal, source);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// `for in` skips non-enumerable keys and allocates no key array, which is why it beats
|
||||
// getOwnPropertyNames here. The own check is needed because it also walks the prototype.
|
||||
for (const key in source) {
|
||||
if (!Object.prototype.hasOwnProperty.call(source, key))
|
||||
continue;
|
||||
// Skip __proto__ properties to prevent prototype pollution
|
||||
if (key === '__proto__')
|
||||
continue;
|
||||
if (onlyProps && !onlyProps.includes(key))
|
||||
continue;
|
||||
dest[key] = cloneRef(source[key], clones, sources, dests);
|
||||
}
|
||||
}
|
||||
for (const key of Object.getOwnPropertySymbols(source)) {
|
||||
if (onlyProps && !onlyProps.includes(key))
|
||||
continue;
|
||||
if (!nonenumerable && !Object.prototype.propertyIsEnumerable.call(source, key))
|
||||
continue;
|
||||
const newVal = cloneRef(source[key], clones, sources, dests);
|
||||
assignProp(dest, key, newVal, source);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user