const CAN_NOT_TARVERSE = [
'[object Boolean]',
'[object Number]',
'[object String]',
'[object Date]',
'[object Function]',
'[object RegExp]',
'[object Symbol]'
]
function getType(target) {
return Object.prototype.toString.call(target)
}
function isReference(target) {
return target !== null && (typeof target === 'object' || typeof target === 'function')
}
function isArray(target) {
return isReference(target) && getType(target) === '[object Array]'
}
function isObject(target) {
return isReference(target) && getType(target) === '[object Object]'
}
function isMap(target) {
return isReference(target) && getType(target) === '[object Map]'
}
function isSet(target) {
return isReference(target) && getType(target) === '[object Set]'
}
function getInitialReference(target) {
return new target.constructor()
}
function getClonedReference(map, originReference) {
let clonedReference
let isCycle = false
if (map.get(originReference)) {
clonedReference = map.get(originReference)
isCycle = true
} else {
clonedReference = CAN_NOT_TARVERSE.includes(getType(originReference))
? getCantTraverseReference(originReference, getType(originReference))
: getInitialReference(originReference)
map.set(originReference, clonedReference)
}
return {
clonedReference,
isCycle
}
}
function getCantTraverseReference(target, type) {
function cloneRegExp(regexp) {
const reFlags = /\w*$/
const result = new regexp.constructor(regexp.source, reFlags.exec(regexp))
result.lastIndex = regexp.lastIndex
return result
}
switch (type) {
case '[object Boolean]':
case '[object Number]':
case '[object String]':
case '[object Date]':
return new target.constructor(target)
case '[object Function]':
return target
case '[object RegExp]':
return cloneRegExp(target)
case '[object Symbol]':
return Object(Symbol.prototype.valueOf.call(target))
default:
return null
}
}
function cloneDeep(target) {
if (!isReference(target)) return target
const map = new Map()
const { clonedReference: rootReference } = getClonedReference(map, target)
const queue = [{ source: target, draft: rootReference }]
while (queue.length) {
const current = queue.shift()
const { source, draft } = current
const sourceType = getType(source)
if (isArray(source)) {
for (const item of source) {
if (!isReference(item)) {
draft.push(item)
} else {
const { isCycle, clonedReference } = getClonedReference(map, item)
draft.push(clonedReference)
!isCycle && queue.push({ source: item, draft: clonedReference })
}
}
} else if (isObject(source)) {
for (const [key, value] of Object.entries(source)) {
if (!isReference(value)) {
draft[key] = value
} else {
const { isCycle, clonedReference } = getClonedReference(map, value)
draft[key] = clonedReference
!isCycle && queue.push({ source: value, draft: clonedReference })
}
}
} else if (isMap(source)) {
for (const [key, value] of Object.entries(source)) {
if (!isReference(value)) {
draft.set(key, value)
} else {
const { isCycle, clonedReference } = getClonedReference(map, value)
draft.set(key, clonedReference)
!isCycle && queue.push({ source: value, draft: clonedReference })
}
}
} else if (isSet(source)) {
for (const value of source) {
if (!isReference(value)) {
draft.add(value)
} else {
const { isCycle, clonedReference } = getClonedReference(map, value)
draft.add(clonedReference)
!isCycle && queue.push({ source: value, draft: clonedReference })
}
}
}
}
return rootReference
}