feat(cloneDeep): Support full feature parity with lodash

This commit is contained in:
raon0211 2024-07-31 21:50:17 +09:00
parent 5c1251c5a5
commit 876931405a
6 changed files with 469 additions and 154 deletions

View File

@ -187,7 +187,7 @@ Even if a feature is marked "in review," it might already be under review to ens
| ---------------------------------------------------------------------- | --------------------- |
| [castArray](https://lodash.com/docs/4.17.15#castArray) | ❌ |
| [clone](https://lodash.com/docs/4.17.15#clone) | 📝 |
| [cloneDeep](https://lodash.com/docs/4.17.15#cloneDeep) | |
| [cloneDeep](https://lodash.com/docs/4.17.15#cloneDeep) | |
| [cloneDeepWith](https://lodash.com/docs/4.17.15#cloneDeepWith) | ❌ |
| [cloneWith](https://lodash.com/docs/4.17.15#cloneWith) | ❌ |
| [conformsTo](https://lodash.com/docs/4.17.15#conformsTo) | ❌ |

View File

@ -188,7 +188,7 @@ chunk([1, 2, 3, 4], 0);
| ---------------------------------------------------------------------- | --------- |
| [castArray](https://lodash.com/docs/4.17.15#castArray) | ❌ |
| [clone](https://lodash.com/docs/4.17.15#clone) | 📝 |
| [cloneDeep](https://lodash.com/docs/4.17.15#cloneDeep) | |
| [cloneDeep](https://lodash.com/docs/4.17.15#cloneDeep) | |
| [cloneDeepWith](https://lodash.com/docs/4.17.15#cloneDeepWith) | ❌ |
| [cloneWith](https://lodash.com/docs/4.17.15#cloneWith) | ❌ |
| [conformsTo](https://lodash.com/docs/4.17.15#conformsTo) | ❌ |

View File

@ -187,7 +187,7 @@ chunk([1, 2, 3, 4], 0);
| ---------------------------------------------------------------------- | -------- |
| [castArray](https://lodash.com/docs/4.17.15#castArray) | ❌ |
| [clone](https://lodash.com/docs/4.17.15#clone) | 📝 |
| [cloneDeep](https://lodash.com/docs/4.17.15#cloneDeep) | |
| [cloneDeep](https://lodash.com/docs/4.17.15#cloneDeep) | |
| [cloneDeepWith](https://lodash.com/docs/4.17.15#cloneDeepWith) | ❌ |
| [cloneWith](https://lodash.com/docs/4.17.15#cloneWith) | ❌ |
| [conformsTo](https://lodash.com/docs/4.17.15#conformsTo) | ❌ |

View File

@ -0,0 +1,291 @@
import { describe, expect, it } from "vitest";
import { cloneDeep } from "./cloneDeep";
import { range } from "../../math/range";
import { LARGE_ARRAY_SIZE } from "../_internal/LARGE_ARRAY_SIZE";
import { args } from "../_internal/args";
import { stubTrue } from "../_internal/stubTrue";
describe('cloneDeep', () => {
it('should deep clone objects with circular references', () => {
const object: any = {
foo: { b: { c: { d: {} } } },
bar: {},
};
object.foo.b.c.d = object;
object.bar.b = object.foo.b;
const actual = cloneDeep(object);
expect(actual.bar.b).toBe(actual.foo.b);
expect(actual).toBe(actual.foo.b.c.d);
expect(actual).not.toBe(object);
});
it('should deep clone objects with lots of circular references', () => {
const cyclical: any = {};
range(LARGE_ARRAY_SIZE + 1).forEach((index) => {
cyclical[`v${index}`] = [index ? cyclical[`v${index - 1}`] : cyclical];
});
const clone = cloneDeep(cyclical);
const actual = clone[`v${LARGE_ARRAY_SIZE}`][0];
expect(actual).toBe(clone[`v${LARGE_ARRAY_SIZE - 1}`]);
expect(actual).not.toBe(cyclical[`v${LARGE_ARRAY_SIZE - 1}`]);
});
class Foo {
a = 1;
}
// eslint-disable-next-line
// @ts-ignore
Foo.prototype.b = 1;
// eslint-disable-next-line
// @ts-ignore
Foo.c = function () { };
var map = new Map();
map.set('a', 1);
map.set('b', 2);
var set = new Set();
set.add(1);
set.add(2);
const objects = {
booleans: false,
'boolean objects': Object(false),
'date objects': new Date(),
'Foo instances': new Foo(),
objects: { a: 0, b: 1, c: 2 },
'objects with object values': { a: /a/, b: ['B'], c: { C: 1 } },
maps: map,
'null values': null,
numbers: 0,
'number objects': Object(0),
regexes: /a/gim,
sets: set,
strings: 'a',
'string objects': Object('a'),
'undefined values': undefined,
};
it(`should clone arguments objects`, () => {
const actual = cloneDeep(args);
expect(actual).toEqual(args);
expect(actual).not.toBe(args);
});
it(`should clone arrays`, () => {
const object = ['a', ''];
const actual = cloneDeep(['a', '']);
expect(actual).toEqual(object);
expect(actual).not.toBe(object);
});
it(`should clone array-like objects`, () => {
const object = { 0: 'a', length: 1 };
const actual = cloneDeep(object);
expect(actual).toEqual(object);
expect(actual).not.toBe(object);
});
it('should clone booleans', () => {
const object = false;
const actual = cloneDeep(object);
expect(actual).toEqual(object);
expect(actual).toBe(object);
});
it('should clone boolean objects', () => {
const object = Object(false);
const actual = cloneDeep(object);
expect(actual).toEqual(object);
expect(actual).not.toBe(object);
});
it('should clone date objects', () => {
const object = new Date();
const actual = cloneDeep(object);
expect(actual).toEqual(object);
expect(actual).not.toBe(object);
});
it('should clone Foo instances', () => {
const object = new Foo();
const actual = cloneDeep(object);
expect(actual).toEqual(object);
expect(actual).not.toBe(object);
});
it('should clone objects', () => {
const object = { a: 0, b: 1, c: 2 };
const actual = cloneDeep(object);
expect(actual).toEqual(object);
expect(actual).not.toBe(object);
});
it('should clone objects with object values', () => {
const object = { a: /a/, b: ['B'], c: { C: 1 } };
const actual = cloneDeep(object);
expect(actual).toEqual(object);
expect(actual).not.toBe(object);
});
it('should clone maps', () => {
const object = map;
const actual = cloneDeep(object);
expect(actual).toEqual(object);
expect(actual).not.toBe(object);
});
it('should clone null values', () => {
const object = null;
const actual = cloneDeep(object);
expect(actual).toEqual(object);
expect(actual).toBe(object);
});
it('should clone numbers', () => {
const object = 0;
const actual = cloneDeep(object);
expect(actual).toEqual(object);
expect(actual).toBe(object);
});
it('should clone number objects', () => {
const object = Object(0);
const actual = cloneDeep(object);
expect(actual).toEqual(object);
expect(actual).not.toBe(object);
});
it('should clone regexes', () => {
const object = /a/gim;
const actual = cloneDeep(object);
expect(actual).toEqual(object);
expect(actual).not.toBe(object);
});
it('should clone sets', () => {
const object = set;
const actual = cloneDeep(object);
expect(actual).toEqual(object);
expect(actual).not.toBe(object);
});
it('should clone strings', () => {
const object = 'a';
const actual = cloneDeep(object);
expect(actual).toEqual(object);
expect(actual).toBe(object);
});
it('should clone string objects', () => {
const object = Object('a');
const actual = cloneDeep(object);
expect(actual).toEqual(object);
expect(actual).not.toBe(object);
});
it('should clone undefined values', () => {
const object = undefined;
const actual = cloneDeep(object);
expect(actual).toEqual(object);
expect(actual).toBe(object);
});
it(`should clone array buffers`, () => {
const arrayBuffer = new ArrayBuffer(2);
const actual = cloneDeep(arrayBuffer);
expect(actual.byteLength).toBe(arrayBuffer.byteLength);
expect(actual).not.toBe(arrayBuffer);
});
it(`should clone buffers`, () => {
const buffer = Buffer.from([1, 2]);
const actual = cloneDeep(buffer);
expect(actual.byteLength).toBe(buffer.byteLength);
// eslint-disable-next-line
// @ts-ignore
expect(actual.inspect()).toBe(buffer.inspect());
expect(actual).not.toBe(buffer);
buffer[0] = 2;
expect(actual[0]).toBe(2);
});
it(`should clone \`index\` and \`input\` array properties`, () => {
const array = /c/.exec('abcde');
const actual = cloneDeep(array);
expect(actual?.index).toBe(2);
expect(actual?.input).toBe('abcde');
});
it(`should clone \`lastIndex\` regexp property`, () => {
const regexp = /c/g;
regexp.exec('abcde');
expect(cloneDeep(regexp).lastIndex).toBe(3);
});
it(`should clone expando properties`, () => {
const values = [false, true, 1, 'a'].map((value) => {
const object = Object(value);
object.a = 1;
return object;
});
const expected = values.map(stubTrue);
const actual = values.map((value) => {
return cloneDeep(value).a === 1
});
expect(actual).toEqual(expected);
});
});

View File

@ -0,0 +1,85 @@
import { cloneDeep as cloneDeepToolkit, copyProperties } from '../../object/cloneDeep.ts';
/**
* Creates a deep clone of the given object.
*
* @template T - The type of the object.
* @param {T} obj - The object to clone.
* @returns {T} - A deep clone of the given object.
*
* @example
* // Clone a primitive values
* const num = 29;
* const clonedNum = clone(num);
* console.log(clonedNum); // 29
* console.log(clonedNum === num) ; // true
*
* @example
* // Clone an array
* const arr = [1, 2, 3];
* const clonedArr = clone(arr);
* console.log(clonedArr); // [1, 2, 3]
* console.log(clonedArr === arr); // false
*
* @example
* // Clone an array with nested objects
* const arr = [1, { a: 1 }, [1, 2, 3]];
* const clonedArr = clone(arr);
* arr[1].a = 2;
* console.log(arr); // [2, { a: 2 }, [1, 2, 3]]
* console.log(clonedArr); // [1, { a: 1 }, [1, 2, 3]]
* console.log(clonedArr === arr); // false
*
* @example
* // Clone an object
* const obj = { a: 1, b: 'es-toolkit', c: [1, 2, 3] };
* const clonedObj = clone(obj);
* console.log(clonedObj); // { a: 1, b: 'es-toolkit', c: [1, 2, 3] }
* console.log(clonedObj === obj); // false
*
* @example
* // Clone an object with nested objects
* const obj = { a: 1, b: { c: 1 } };
* const clonedObj = clone(obj);
* obj.b.c = 2;
* console.log(obj); // { a: 1, b: { c: 2 } }
* console.log(clonedObj); // { a: 1, b: { c: 1 } }
* console.log(clonedObj === obj); // false
*/
export function cloneDeep<T>(obj: T): T {
if (typeof obj !== 'object') {
return cloneDeepToolkit(obj);
}
switch (Object.prototype.toString.call(obj)) {
case '[object Number]':
case '[object String]':
case '[object Boolean]': {
// eslint-disable-next-line
// @ts-ignore
const result = new obj.constructor(obj?.valueOf()) as T;
copyProperties(result, obj);
return result;
}
case '[object Arguments]': {
const result = {} as any;
copyProperties(result, obj);
// eslint-disable-next-line
// @ts-ignore
result.length = obj.length;
// eslint-disable-next-line
// @ts-ignore
result[Symbol.iterator] = obj[Symbol.iterator];
return result as T;
}
default: {
return cloneDeepToolkit(obj);
}
}
}

View File

@ -1,3 +1,6 @@
import { isPrimitive } from "../predicate/isPrimitive.ts";
import { isTypedArray } from "../predicate/isTypedArray.ts";
/**
* Creates a deep clone of the given object.
*
@ -44,223 +47,159 @@
* console.log(clonedObj); // { a: 1, b: { c: 1 } }
* console.log(clonedObj === obj); // false
*/
export function cloneDeep<T>(obj: T): Resolved<T> {
export function cloneDeep<T>(obj: T): T {
return cloneDeepImpl(obj);
}
function cloneDeepImpl<T>(obj: T, stack = new Map<any, any>()): T {
if (isPrimitive(obj)) {
return obj as Resolved<T>;
return obj as T;
}
if (stack.has(obj)) {
return stack.get(obj) as T;
}
if (Array.isArray(obj)) {
return obj.map(item => cloneDeep(item)) as Resolved<T>;
const result: any = new Array(obj.length);
stack.set(obj, result);
for (let i = 0; i < obj.length; i++) {
result[i] = cloneDeepImpl(obj[i], stack);
}
// For RegExpArrays
if (obj.hasOwnProperty('index')) {
// eslint-disable-next-line
// @ts-ignore
result.index = obj.index;
}
if (obj.hasOwnProperty('input')) {
// eslint-disable-next-line
// @ts-ignore
result.input = obj.input;
}
return result as T;
}
if (obj instanceof Date) {
return new Date(obj.getTime()) as Resolved<T>;
return new Date(obj.getTime()) as T;
}
if (obj instanceof RegExp) {
return new RegExp(obj.source, obj.flags) as Resolved<T>;
const result = new RegExp(obj.source, obj.flags);
result.lastIndex = obj.lastIndex;
return result as T;
}
if (obj instanceof Map) {
const result = new Map();
stack.set(obj, result);
for (const [key, value] of obj.entries()) {
result.set(key, cloneDeep(value));
result.set(key, cloneDeepImpl(value, stack));
}
return result as Resolved<T>;
return result as T;
}
if (obj instanceof Set) {
const result = new Set();
stack.set(obj, result);
for (const value of obj.values()) {
result.add(cloneDeep(value));
result.add(cloneDeepImpl(value, stack));
}
return result as Resolved<T>;
return result as T;
}
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(obj)) {
return obj.subarray() as T;
}
if (isTypedArray(obj)) {
const result = new (Object.getPrototypeOf(obj).constructor)(obj.length);
stack.set(obj, result);
for (let i = 0; i < obj.length; i++) {
result[i] = cloneDeep(obj[i]);
result[i] = cloneDeepImpl(obj[i], stack);
}
return result as Resolved<T>;
return result as T;
}
if (obj instanceof ArrayBuffer || (typeof SharedArrayBuffer !== 'undefined' && obj instanceof SharedArrayBuffer)) {
return obj.slice(0) as Resolved<T>;
return obj.slice(0) as T;
}
if (obj instanceof DataView) {
const result = new DataView(obj.buffer.slice(0));
cloneDeepHelper(obj, result);
return result as Resolved<T>;
stack.set(obj, result);
copyProperties(result, obj, stack);
return result as T;
}
// For legacy NodeJS support
if (typeof File !== 'undefined' && obj instanceof File) {
const result = new File([obj], obj.name, { type: obj.type });
cloneDeepHelper(obj, result);
return result as Resolved<T>;
stack.set(obj, result);
copyProperties(result, obj, stack);
return result as T;
}
if (obj instanceof Blob) {
const result = new Blob([obj], { type: obj.type });
cloneDeepHelper(obj, result);
return result as Resolved<T>;
stack.set(obj, result);
copyProperties(result, obj, stack);
return result as T;
}
if (obj instanceof Error) {
const result = new (obj.constructor as { new(): Error })();
stack.set(obj, result);
result.message = obj.message;
result.name = obj.name;
result.stack = obj.stack;
result.cause = obj.cause;
cloneDeepHelper(obj, result);
return result as Resolved<T>;
copyProperties(result, obj, stack);
return result as T;
}
if (typeof obj === 'object' && obj !== null) {
const result = {};
cloneDeepHelper(obj, result);
return result as Resolved<T>;
stack.set(obj, result);
copyProperties(result, obj, stack);
return result as T;
}
return obj as Resolved<T>;
}
type Primitive = null | undefined | string | number | boolean | symbol | bigint;
function isPrimitive(value: unknown): value is Primitive {
return value == null || (typeof value !== 'object' && typeof value !== 'function');
return obj;
}
// eslint-disable-next-line
function cloneDeepHelper(obj: any, clonedObj: any): void {
const keys = Object.keys(obj);
export function copyProperties(target: any, source: any, stack?: Map<any, any>): void {
const keys = Object.keys(source);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
const descriptor = Object.getOwnPropertyDescriptor(source, key);
if ((descriptor?.writable || descriptor?.set)) {
clonedObj[key] = cloneDeep(obj[key]);
target[key] = cloneDeepImpl(source[key], stack);
}
}
}
function isTypedArray(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
obj: any
): obj is
| Uint8Array
| Uint8ClampedArray
| Uint16Array
| Uint32Array
| BigUint64Array
| Int8Array
| Int16Array
| Int32Array
| BigInt64Array
| Float32Array
| Float64Array {
return (
obj instanceof Uint8Array ||
obj instanceof Uint8ClampedArray ||
obj instanceof Uint16Array ||
obj instanceof Uint32Array ||
obj instanceof BigUint64Array ||
obj instanceof Int8Array ||
obj instanceof Int16Array ||
obj instanceof Int32Array ||
obj instanceof BigInt64Array ||
obj instanceof Float32Array ||
obj instanceof Float64Array
);
}
export type Resolved<T> = Equal<T, ResolvedMain<T>> extends true ? T : ResolvedMain<T>;
type Equal<X, Y> = X extends Y ? (Y extends X ? true : false) : false;
type ResolvedMain<T> = T extends [never]
? never // (special trick for jsonable | null) type
: ValueOf<T> extends boolean | number | bigint | string
? ValueOf<T>
: T extends (...args: any[]) => any
? never
: T extends object
? ResolvedObject<T>
: ValueOf<T>;
type ResolvedObject<T extends object> =
T extends Array<infer U>
? IsTuple<T> extends true
? ResolvedTuple<T>
: Array<ResolvedMain<U>>
: T extends Set<infer U>
? Set<ResolvedMain<U>>
: T extends Map<infer K, infer V>
? Map<ResolvedMain<K>, ResolvedMain<V>>
: T extends WeakSet<any> | WeakMap<any, any>
? never
: T extends
| Date
| Uint8Array
| Uint8ClampedArray
| Uint16Array
| Uint32Array
| BigUint64Array
| Int8Array
| Int16Array
| Int32Array
| BigInt64Array
| Float32Array
| Float64Array
| ArrayBuffer
| SharedArrayBuffer
| DataView
| Blob
| File
? T
: {
[P in keyof T]: ResolvedMain<T[P]>;
};
type ResolvedTuple<T extends readonly any[]> = T extends []
? []
: T extends [infer F]
? [ResolvedMain<F>]
: T extends [infer F, ...infer Rest extends readonly any[]]
? [ResolvedMain<F>, ...ResolvedTuple<Rest>]
: T extends [(infer F)?]
? [ResolvedMain<F>?]
: T extends [(infer F)?, ...infer Rest extends readonly any[]]
? [ResolvedMain<F>?, ...ResolvedTuple<Rest>]
: [];
type IsTuple<T extends readonly any[] | { length: number }> = [T] extends [never]
? false
: T extends readonly any[]
? number extends T['length']
? false
: true
: false;
type ValueOf<Instance> =
IsValueOf<Instance, boolean> extends true
? boolean
: IsValueOf<Instance, number> extends true
? number
: IsValueOf<Instance, string> extends true
? string
: Instance;
type IsValueOf<Instance, O extends IValueOf<any>> = Instance extends O
? O extends IValueOf<infer Primitive>
? Instance extends Primitive
? false
: true // not Primitive, but Object
: false // cannot be
: false;
interface IValueOf<T> {
valueOf(): T;
}