-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
2231c0e
commit 46ee7c7
Showing
5 changed files
with
397 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import * as _ from 'radashi' | ||
import { bench } from 'vitest' | ||
|
||
describe('cloneDeep', () => { | ||
const objects: any = _.list(0, 5, i => { | ||
const object: any = {} | ||
_.set(object, 'a.b.c.d.e.f.g.h.i.k.l.m.n.o.p', i) | ||
return object | ||
}) | ||
|
||
bench('dozens of nested plain objects', () => { | ||
_.cloneDeep(objects) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
--- | ||
title: cloneDeep | ||
description: Create a deep copy of an object or array | ||
--- | ||
|
||
### Usage | ||
|
||
Deeply clone the given object or array. The only nested objects that get cloned by default are: plain objects, arrays, `Map` instances, and `Set` instances. | ||
|
||
The default behavior aims to support the most popular use cases. See “Customized cloning” below if you need more control. | ||
|
||
By default, non-enumerable properties and computed properties are copied losslessly. Note that you can opt out of this behavior if you need better performance (see “Faster cloning” below). | ||
|
||
```ts | ||
import * as _ from 'radashi' | ||
|
||
_.cloneDeep() | ||
``` | ||
|
||
### Faster cloning | ||
|
||
You can pass the `FastCloningStrategy` for better performance, but bear in mind the following tradeoff. | ||
|
||
All plain objects and class instances are cloned with `{...obj}`. This means that the original prototype, computed properties, and non-enumerable properties are not preserved. | ||
|
||
Also note that built-in, complex objects like `RegExp` and `Date` are still not cloned with this cloning strategy. You can override the `cloneOther` function if you need to clone these object types. | ||
|
||
### Customized cloning | ||
|
||
“Cloning strategies” control how certain object types are handled by `cloneDeep`. You can pass in a custom strategy, which may even be a partial strategy. Any undefined methods in your strategy will inherit the default logic. Your custom methods can return `null` to use the default logic, or they can return the received object to skip cloning. | ||
|
||
```ts | ||
import * as _ from 'radashi' | ||
|
||
_.cloneDeep(obj, { | ||
// Clone arrays with default logic if they are not frozen. | ||
cloneArray: array => (Object.isFrozen(array) ? array : null), | ||
}) | ||
``` | ||
|
||
If you clone the object in your custom method, make sure to pass the clone into the `track` function before cloning the nested objects. Here's an example with `cloneOther` that handles a custom class instance. | ||
|
||
```ts | ||
import * as _ from 'radashi' | ||
|
||
_.cloneDeep(obj, { | ||
cloneOther: (obj, track, clone) => { | ||
if (obj instanceof MyClass) { | ||
// 1. Create a new instance and track it. | ||
const clone = track(new MyClass()) | ||
|
||
// 2. Copy over the properties of the original instance. | ||
for (const key in obj) { | ||
clone[key] = clone(obj[key]) | ||
} | ||
|
||
// 3. Return the cloned instance. | ||
return clone | ||
} | ||
|
||
// Use default logic for anything else. | ||
return null | ||
}, | ||
}) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
import { isArray, isMap, isObject, isSet } from 'radashi' | ||
|
||
/** | ||
* A strategy for cloning objects with `cloneDeep`. | ||
* | ||
* Methods **must** call the `track` function with the new parent | ||
* object **before** looping over the input object's | ||
* properties/elements for cloning purposes. This protects against | ||
* circular references. | ||
* | ||
* Methods may return the input object to indicate that cloning should | ||
* be skipped. | ||
* | ||
* Methods may return null to indicate that the default cloning logic | ||
* should be used. | ||
*/ | ||
export interface CloningStrategy { | ||
cloneMap: <K, V>( | ||
parent: Map<K, V>, | ||
track: (newParent: Map<K, V>) => Map<K, V>, | ||
clone: <T>(value: T) => T, | ||
) => Map<K, V> | null | ||
cloneSet: <T>( | ||
parent: Set<T>, | ||
track: (newParent: Set<T>) => Set<T>, | ||
clone: <T>(value: T) => T, | ||
) => Set<T> | null | ||
cloneArray: <T>( | ||
parent: readonly T[], | ||
track: (newParent: T[]) => T[], | ||
clone: <T>(value: T) => T, | ||
) => T[] | null | ||
cloneObject: <T extends object>( | ||
parent: T, | ||
track: (newParent: T) => T, | ||
clone: <T>(value: T) => T, | ||
) => T | null | ||
cloneOther: <T>( | ||
parent: T, | ||
track: (newParent: T) => T, | ||
clone: <T>(value: T) => T, | ||
) => T | null | ||
} | ||
|
||
export const DefaultCloningStrategy = { | ||
cloneMap<K, V>( | ||
input: Map<K, V>, | ||
track: (newParent: Map<K, V>) => Map<K, V>, | ||
clone: <T>(value: T) => T, | ||
): Map<K, V> { | ||
const output = track(new Map()) | ||
for (const [key, value] of input) { | ||
output.set(key, clone(value)) | ||
} | ||
return output | ||
}, | ||
cloneSet<T>( | ||
input: Set<T>, | ||
track: (newParent: Set<T>) => Set<T>, | ||
clone: <T>(value: T) => T, | ||
): Set<T> { | ||
const output = track(new Set()) | ||
for (const value of input) { | ||
output.add(clone(value)) | ||
} | ||
return output | ||
}, | ||
cloneArray<T>( | ||
input: readonly T[], | ||
track: (newParent: T[]) => T[], | ||
clone: <T>(value: T) => T, | ||
): T[] { | ||
// Use .forEach for correct handling of sparse arrays | ||
const output = track(new Array(input.length)) | ||
input.forEach((value, index) => { | ||
output[index] = clone(value) | ||
}) | ||
return output | ||
}, | ||
cloneObject<T extends object>( | ||
input: T, | ||
track: (newParent: T) => T, | ||
clone: <T>(value: T) => T, | ||
): T { | ||
const output = track(Object.create(Object.getPrototypeOf(input))) | ||
for (const key of Reflect.ownKeys(input)) { | ||
// By copying the property descriptors, we preserve computed | ||
// properties and non-enumerable properties. | ||
const descriptor = Object.getOwnPropertyDescriptor(input, key)! | ||
if ('value' in descriptor) { | ||
descriptor.value = clone(descriptor.value) | ||
} | ||
Object.defineProperty(output, key, descriptor) | ||
} | ||
return output | ||
}, | ||
cloneOther<T>(input: T, track: (newParent: T) => T): T { | ||
return track(input) | ||
}, | ||
} | ||
|
||
/** | ||
* If you don't need support for non-enumerable properties or computed | ||
* properties, and you're not using custom classes, you can use this | ||
* strategy for better performance. | ||
*/ | ||
export const FastCloningStrategy = { | ||
cloneObject: <T extends object>( | ||
input: T, | ||
track: (newParent: T) => T, | ||
clone: <T>(value: T) => T, | ||
): T => { | ||
const output: any = track({ ...input }) | ||
for (const key of Object.keys(input)) { | ||
output[key] = clone(input[key as keyof object]) | ||
} | ||
return output | ||
}, | ||
} | ||
|
||
/** | ||
* Clone the given object and possibly other objects nested inside. | ||
* | ||
* By default, the only objects that get cloned are plain objects, | ||
* class instances, arrays, `Set` instances, and `Map` instances. If | ||
* an object is not cloned, any objects nested inside are also not | ||
* cloned. | ||
* | ||
* You may define a custom cloning strategy by passing a partial | ||
* implementation of the `CloningStrategy` interface to the | ||
* `cloneDeep` function. Any undefined methods will fall back to the | ||
* default cloning logic. Your own methods may return null to indicate | ||
* that the default cloning logic should be used. They may also return | ||
* the input object to indicate that cloning should be skipped. | ||
* | ||
* ```ts | ||
* const obj = { a: 1, b: { c: 2 } } | ||
* const clone = cloneDeep(obj) | ||
* | ||
* assert(clone !== obj) | ||
* assert(clone.b !== obj.b) | ||
* assert(JSON.stringify(clone) === JSON.stringify(obj)) | ||
* ``` | ||
*/ | ||
export function cloneDeep<T extends object>( | ||
root: T, | ||
customStrategy?: Partial<CloningStrategy>, | ||
): T { | ||
const strategy = { ...DefaultCloningStrategy, ...customStrategy } | ||
|
||
const tracked = new Map<unknown, unknown>() | ||
const track = (parent: unknown, newParent: unknown) => { | ||
tracked.set(parent, newParent) | ||
return newParent | ||
} | ||
|
||
const clone = <T>(value: T): T => | ||
value && typeof value === 'object' | ||
? ((tracked.get(value) ?? cloneDeep(value, strategy)) as T) | ||
: value | ||
|
||
const cloneDeep = (parent: unknown, strategy: CloningStrategy): unknown => { | ||
const cloneParent = ( | ||
isObject(parent) | ||
? strategy.cloneObject | ||
: isArray(parent) | ||
? strategy.cloneArray | ||
: isMap(parent) | ||
? strategy.cloneMap | ||
: isSet(parent) | ||
? strategy.cloneSet | ||
: strategy.cloneOther | ||
) as ( | ||
newParent: unknown, | ||
track: (newParent: unknown) => unknown, | ||
clone: (value: unknown) => unknown, | ||
) => unknown | ||
|
||
const newParent = cloneParent(parent, track.bind(null, parent), clone) | ||
if (!newParent) { | ||
// Use the default strategy if null is returned. | ||
return cloneDeep(parent, DefaultCloningStrategy) | ||
} | ||
|
||
tracked.set(parent, newParent) | ||
return newParent | ||
} | ||
|
||
return cloneDeep(root, strategy) as T | ||
} |
Oops, something went wrong.