feat: implement deepClone function for deep cloning of objects and arrays
This commit is contained in:
145
lib/shared/helpers/deep-clone.test.ts
Normal file
145
lib/shared/helpers/deep-clone.test.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { deepClone } from '@/shared/helpers/deep-clone'
|
||||||
|
|
||||||
|
describe('deepClone', () => {
|
||||||
|
it('clona valores primitivos', () => {
|
||||||
|
expect(deepClone(42)).toBe(42)
|
||||||
|
expect(deepClone('abc')).toBe('abc')
|
||||||
|
expect(deepClone(true)).toBe(true)
|
||||||
|
expect(deepClone(null)).toBeNull()
|
||||||
|
expect(deepClone(undefined)).toBeUndefined()
|
||||||
|
expect(deepClone(123n)).toBe(123n)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clona arrays y objetos anidados (referencias diferentes)', () => {
|
||||||
|
const obj = { a: 1, b: { c: 2 } }
|
||||||
|
const arr = [1, { x: 2 }]
|
||||||
|
const src = { obj, arr }
|
||||||
|
|
||||||
|
const cloned = deepClone(src)
|
||||||
|
expect(cloned).not.toBe(src)
|
||||||
|
expect(cloned.obj).not.toBe(src.obj)
|
||||||
|
expect(cloned.arr).not.toBe(src.arr)
|
||||||
|
expect(cloned).toEqual(src)
|
||||||
|
|
||||||
|
// mutar clon no debe afectar al original
|
||||||
|
cloned.obj.b.c = 99
|
||||||
|
expect(src.obj.b.c).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('maneja Date y RegExp', () => {
|
||||||
|
const d = new Date()
|
||||||
|
const r = /abc/gi
|
||||||
|
|
||||||
|
const cd = deepClone(d)
|
||||||
|
const cr = deepClone(r)
|
||||||
|
|
||||||
|
expect(cd).not.toBe(d)
|
||||||
|
expect(cd.getTime()).toBe(d.getTime())
|
||||||
|
|
||||||
|
expect(cr).not.toBe(r)
|
||||||
|
expect(cr.source).toBe(r.source)
|
||||||
|
expect(cr.flags).toBe(r.flags)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clona Map y Set (clonando claves/valores)', () => {
|
||||||
|
const keyObj = { k: 'v' }
|
||||||
|
const m = new Map<any, any>([[keyObj, { nested: 1 }]])
|
||||||
|
const s = new Set([keyObj, 2, 'x'])
|
||||||
|
|
||||||
|
const cm = deepClone(m)
|
||||||
|
const cs = deepClone(s)
|
||||||
|
|
||||||
|
expect(cm).not.toBe(m)
|
||||||
|
expect(cs).not.toBe(s)
|
||||||
|
|
||||||
|
// Map: la clave será un objeto distinto pero con los mismos datos
|
||||||
|
const [[clonedKey, clonedVal]] = Array.from(cm.entries())
|
||||||
|
expect(clonedKey).not.toBe(keyObj)
|
||||||
|
expect(clonedKey).toEqual(keyObj)
|
||||||
|
expect(clonedVal).toEqual({ nested: 1 })
|
||||||
|
|
||||||
|
// Set: debe contener un objeto equivalente al original
|
||||||
|
const found = Array.from(cs.values()).find(
|
||||||
|
(v) => typeof v === 'object' && (v as any).k === 'v',
|
||||||
|
)
|
||||||
|
expect(found).toBeDefined()
|
||||||
|
expect(found).not.toBe(keyObj)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clona TypedArray, ArrayBuffer y DataView', () => {
|
||||||
|
const buf = new ArrayBuffer(8)
|
||||||
|
const dv = new DataView(buf)
|
||||||
|
dv.setInt8(0, 42)
|
||||||
|
const ta = new Uint8Array([1, 2, 3])
|
||||||
|
|
||||||
|
const cbuf = deepClone(buf)
|
||||||
|
const cdv = deepClone(dv)
|
||||||
|
const cta = deepClone(ta)
|
||||||
|
|
||||||
|
expect(cbuf).not.toBe(buf)
|
||||||
|
expect(cdv).not.toBe(dv)
|
||||||
|
expect(cdv.getInt8(0)).toBe(42)
|
||||||
|
|
||||||
|
expect(cta).not.toBe(ta)
|
||||||
|
expect(Array.from(cta)).toEqual([1, 2, 3])
|
||||||
|
expect(cta).toBeInstanceOf(Uint8Array)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('preserva prototype y métodos de instancia', () => {
|
||||||
|
class C {
|
||||||
|
a = 1
|
||||||
|
method() {
|
||||||
|
return this.a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const inst = new C()
|
||||||
|
inst.a = 5
|
||||||
|
|
||||||
|
const cloned = deepClone(inst as any)
|
||||||
|
expect(cloned).not.toBe(inst)
|
||||||
|
expect(cloned).toBeInstanceOf(C)
|
||||||
|
expect(cloned.method()).toBe(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('mantiene referencias a funciones (no las clona)', () => {
|
||||||
|
const fn = () => 1
|
||||||
|
const src = { fn }
|
||||||
|
const cloned = deepClone(src as any)
|
||||||
|
expect(cloned.fn).toBe(fn)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('maneja referencias circulares', () => {
|
||||||
|
const a: any = { name: 'a' }
|
||||||
|
a.self = a
|
||||||
|
|
||||||
|
const c = deepClone(a)
|
||||||
|
expect(c).not.toBe(a)
|
||||||
|
expect(c.self).toBe(c)
|
||||||
|
expect(c.name).toBe('a')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('preserva descriptores y propiedades con símbolos', () => {
|
||||||
|
const s = Symbol('sym')
|
||||||
|
const obj: any = {}
|
||||||
|
|
||||||
|
Object.defineProperty(obj, 'hidden', {
|
||||||
|
value: 42,
|
||||||
|
enumerable: false,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
})
|
||||||
|
obj[s] = { foo: 'bar' }
|
||||||
|
|
||||||
|
const c = deepClone(obj)
|
||||||
|
|
||||||
|
const desc = Object.getOwnPropertyDescriptor(c, 'hidden')!
|
||||||
|
expect(desc.enumerable).toBe(false)
|
||||||
|
expect(desc.value).toBe(42)
|
||||||
|
|
||||||
|
const symKeys = Object.getOwnPropertySymbols(c)
|
||||||
|
expect(symKeys.length).toBe(1)
|
||||||
|
expect(c[s]).toEqual({ foo: 'bar' })
|
||||||
|
expect(c[s]).not.toBe(obj[s])
|
||||||
|
})
|
||||||
|
})
|
||||||
93
lib/shared/helpers/deep-clone.ts
Normal file
93
lib/shared/helpers/deep-clone.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
export function deepClone<T>(value: T): T {
|
||||||
|
const seen = new WeakMap<any, any>()
|
||||||
|
|
||||||
|
const getRegExpFlags = (r: RegExp) => {
|
||||||
|
let flags = ''
|
||||||
|
if (r.global) flags += 'g'
|
||||||
|
if (r.ignoreCase) flags += 'i'
|
||||||
|
if (r.multiline) flags += 'm'
|
||||||
|
if (r.dotAll) flags += 's'
|
||||||
|
if (r.unicode) flags += 'u'
|
||||||
|
if (r.sticky) flags += 'y'
|
||||||
|
return flags
|
||||||
|
}
|
||||||
|
|
||||||
|
function _clone(v: any): any {
|
||||||
|
if (v === null || typeof v !== 'object') {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seen.has(v)) {
|
||||||
|
return seen.get(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v instanceof Date) {
|
||||||
|
return new Date(v.getTime())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v instanceof RegExp) {
|
||||||
|
return new RegExp(v.source, getRegExpFlags(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v instanceof ArrayBuffer) {
|
||||||
|
return v.slice(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ArrayBuffer.isView(v)) {
|
||||||
|
if (v instanceof DataView) {
|
||||||
|
const buf = _clone(v.buffer)
|
||||||
|
return new DataView(buf, v.byteOffset, v.byteLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new (v.constructor as any)(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v instanceof Map) {
|
||||||
|
const m = new Map()
|
||||||
|
seen.set(v, m)
|
||||||
|
for (const [k, val] of v.entries()) {
|
||||||
|
m.set(_clone(k), _clone(val))
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v instanceof Set) {
|
||||||
|
const s = new Set()
|
||||||
|
seen.set(v, s)
|
||||||
|
for (const item of v.values()) s.add(_clone(item))
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(v)) {
|
||||||
|
const arr: any[] = []
|
||||||
|
seen.set(v, arr)
|
||||||
|
for (let i = 0; i < v.length; i++) arr[i] = _clone(v[i])
|
||||||
|
return arr as any
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof v === 'function') {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
const proto = Object.getPrototypeOf(v)
|
||||||
|
const out = Object.create(proto)
|
||||||
|
seen.set(v, out)
|
||||||
|
|
||||||
|
const descriptors = Object.getOwnPropertyDescriptors(v)
|
||||||
|
for (const [key, desc] of Object.entries(descriptors)) {
|
||||||
|
if ('value' in desc) desc.value = _clone(desc.value)
|
||||||
|
Object.defineProperty(out, key, desc as PropertyDescriptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
const symbols = Object.getOwnPropertySymbols(v)
|
||||||
|
for (const s of symbols) {
|
||||||
|
const sd = Object.getOwnPropertyDescriptor(v, s)!
|
||||||
|
if (sd && 'value' in sd) sd.value = _clone(sd.value)
|
||||||
|
Object.defineProperty(out, s, sd as PropertyDescriptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
return _clone(value) as T
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user