深入深拷贝

JSON方法

const cloneDeep_JSON = (obj) => JSON.parse(JSON.stringify(obj))
  • 不能序列化function
  • 处理RegExp、Error只能得到空对象
  • 处理Date对象只能得到时间字符串
  • NaN、Infinity和-Infinity转为Null
  • 会忽略undefined和Symbol
  • 对象中存在循环引用的情况无法正确实现深拷贝

使用Object.assign

const cloneDeep_Assign = (obj) => Object.assign({}, obj)
  • 只能深拷贝第一层

使用MessageChannel (opens new window)

const cloneDeep_MessageChannel = (obj) => new Promise((resolve) => {
    const { port1, port2 } = new MessageChannel()
    port1.onmessage = e => resolve(e.data)
    port2.postMessage(obj)
})
  • 可以在web worker中使用
  • 异步
  • 不能clone函数

深度优先

const cloneDeep_DFS = (obj, visitedArr = []) => {
    let _obj = {}
    const type = Object.prototype.toString.call(obj)
    if (['[object Array]', '[object Object]'].includes(type)) {
        let index = visitedArr.indexOf(obj)
        _obj = type === '[object Array]' ? [] : {}
        if (~index) { // 判断是否循环引用
            _obj = visitedArr[index]
        } else {
            visitedArr.push(obj)
            for (let item in obj) {
                _obj[item] = cloneDeep_DFS(obj[item], visitedArr)
            }
        }
    } else if (type === '[object Function]') {
        _obj = new Function('return ' + obj.toString())
    } else {
        _obj = obj
    }
    return _obj
}

广度优先

const cloneDeep_BFS = (obj) => {
    const queue = [obj]

    const copyObj = {}
    const copyQueue = [copyObj]
    const visitedArr = []

    while (queue.length > 0) {
        const items = queue.shift()
        const _obj = copyQueue.shift()
        const type = Object.prototype.toString.call(items)
        if (['[object Array]', '[object Object]'].includes(type)) {
            for (let key in items) {
                const value = items[key]
                if (Object.prototype.toString.call(value) === '[object Object]') {
                    const index = visitedArr.indexOf(value)
                    if (!~index) {
                        _obj[key] = {}
                        queue.push(value)
                        copyQueue.push(_obj[key])
                        visitedArr.push(value);
                    } else {
                        _obj[key] = visitedArr[index]
                    }
                } else if (Object.prototype.toString.call(value) === '[object Array]') {
                    _obj[key] = []
                    queue.push(value)
                    copyQueue.push(_obj[key])
                } else if (Object.prototype.toString.call(value) === '[object Function]') {
                    _obj[key] = new Function('return ' + value.toString())
                } else {
                    _obj[key] = value
                }
            }
        } else if (type === '[object Function]') {
            _obj = new Function('return ' + items.toString())
        } else {
            _obj = obj
        }
    }
    return copyObj
}

_.cloneDeep(lodash@4.17.15)

  • 省略typedArray部分克隆
const MAX_SAFE_INTEGER = 9007199254740991

const argsTag = '[object Arguments]',
    arrayTag = '[object Array]',
    boolTag = '[object Boolean]',
    dateTag = '[object Date]',
    errorTag = '[object Error]',
    funcTag = '[object Function]',
    genTag = '[object GeneratorFunction]',
    asyncTag = '[object AsyncFunction]',
    proxyTag = '[object Proxy]',
    mapTag = '[object Map]',
    numberTag = '[object Number]',
    objectTag = '[object Object]',
    regexpTag = '[object RegExp]',
    setTag = '[object Set]',
    stringTag = '[object String]',
    symbolTag = '[object Symbol]',
    weakMapTag = '[object WeakMap]'


const arrayBufferTag = '[object ArrayBuffer]',
    dataViewTag = '[object DataView]',
    float32Tag = '[object Float32Array]',
    float64Tag = '[object Float64Array]',
    int8Tag = '[object Int8Array]',
    int16Tag = '[object Int16Array]',
    int32Tag = '[object Int32Array]',
    uint8Tag = '[object Uint8Array]',
    uint8ClampedTag = '[object Uint8ClampedArray]',
    uint16Tag = '[object Uint16Array]',
    uint32Tag = '[object Uint32Array]'

/** Used to identify `toStringTag` values supported by `_.clone`. */
const cloneableTags = {}
cloneableTags[argsTag] = cloneableTags[arrayTag] =
    cloneableTags[arrayBufferTag] = cloneableTags[dataViewTag] =
    cloneableTags[boolTag] = cloneableTags[dateTag] =
    cloneableTags[float32Tag] = cloneableTags[float64Tag] =
    cloneableTags[int8Tag] = cloneableTags[int16Tag] =
    cloneableTags[int32Tag] = cloneableTags[mapTag] =
    cloneableTags[numberTag] = cloneableTags[objectTag] =
    cloneableTags[regexpTag] = cloneableTags[setTag] =
    cloneableTags[stringTag] = cloneableTags[symbolTag] =
    cloneableTags[uint8Tag] = cloneableTags[uint8ClampedTag] =
    cloneableTags[uint16Tag] = cloneableTags[uint32Tag] = true
cloneableTags[errorTag] = cloneableTags[funcTag] =
    cloneableTags[weakMapTag] = false

const isObject = (value) => {
    const type = typeof value
    return value != null && (type == 'object' || type == 'function')
}

const initCloneArray = (array) => {
    const length = array.length,
        result = new array.constructor(length)

    if (length && typeof array[0] == 'string' && Object.prototype.hasOwnProperty.call(array, 'index')) {
        result.index = array.index
        result.input = array.input
    }
    return result
}

const symToStringTag = Symbol ? Symbol.toStringTag : undefined

const getTag = (value) => {
    const nullTag = '[object Null]',
        undefinedTag = '[object Undefined]'

    if (value == null) {
        return value === undefined ? undefinedTag : nullTag
    }
    return (symToStringTag && symToStringTag in Object(value)) ?
        getRawTag(value) :
        Object.prototype.toString.call(value)
}

const getRawTag = (value) => {
    const isOwn = Object.prototype.hasOwnProperty.call(value, symToStringTag),
        tag = value[symToStringTag]
    let unmasked

    try {
        value[symToStringTag] = undefined
        unmasked = true
    } catch (e) { }
    const result = Object.prototype.toString.call(value)
    if (unmasked) {
        if (isOwn) {
            value[symToStringTag] = tag
        } else {
            delete value[symToStringTag]
        }
    }
    return result
}

const baseCreate = (proto) => {
    if (!isObject(proto)) {
        return {}
    }
    return Object.create(proto)
}

const initCloneObject = (object) => {
    const Ctor = object.constructor
    return (typeof Ctor == 'function' && !(Ctor === Ctor.prototype))
        ? baseCreate(Object.getPrototypeOf(Object(object)))
        : {}
}

const cloneArrayBuffer = (arrayBuffer) => {
    const result = new arrayBuffer.constructor(arrayBuffer.byteLength)
    new Uint8Array(result).set(new Uint8Array(arrayBuffer))
    return result
}

const cloneDataView = (dataView) => {
    const buffer = cloneArrayBuffer(dataView.buffer)
    return new dataView.constructor(buffer, dataView.byteLength)
}

const cloneTypedArray = (typedArray) => {
    const buffer = cloneArrayBuffer(typedArray.buffer)
    return new typedArray.constructor(buffer, typedArray.byteLength)
}

const initCloneByTag = (object, tag) => {
    const Ctor = object.constructor
    switch (tag) {
        case arrayBufferTag:
            return cloneArrayBuffer(object)
        case boolTag:
        case dateTag:
            return new Ctor(+object)
        case dataViewTag:
            return cloneDataView(object)
        case float32Tag:
        case float64Tag:
        case int8Tag:
        case int16Tag:
        case int32Tag:
        case uint8Tag:
        case uint8ClampedTag:
        case uint16Tag:
        case uint32Tag:
            return cloneTypedArray(object)
        case mapTag:
        case setTag:
            return new Ctor
        case numberTag:
        case stringTag:
            return new Ctor(object)
        case symbolTag:
            return Object(Symbol.prototype.valueOf.call(object))
    }
}

const eq = (value, other) => {
    return value === other || (value !== value && other !== other)
}

const assocIndexOf = (array, key) => {
    let length = array.length
    while (length--) {
        const value = array[length][0]
        if (eq(value, key)) {
            return length
        }
    }
    return -1
}

class ListCache {
    constructor(entries) {
        let index = -1
        const length = entries == null ? 0 : entries.length
        this.clear()

        while (++index < length) {
            const entry = entries[index]
            this.set(entry[0], entry[1])
        }
    }
    clear() {
        this.__data__ = []
        this.size = 0
    }
    delete(key) {
        const data = this.__data__,
            index = assocIndexOf(data, key)

        if (index < 0) return false
        const lastIndex = data.length - 1
        if (index === lastIndex) data.pop()
        else Array.prototype.splice.call(data, index, 1)
        --this.size
        return true
    }
    get(key) {
        const data = this.__data__,
            index = assocIndexOf(data, key)

        return index < 0 ? undefined : data[index][1]
    }
    has(key) {
        return assocIndexOf(this.__data__, key) > -1
    }
    set(key, value) {
        const data = this.__data__,
            index = assocIndexOf(data, key)

        if (index < 0) {
            ++this.size
            data.push([key, value])
        } else {
            data[index][1] = value
        }
    }
}

const isKeyable = (value) => {
    const type = typeof value
    return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean')
        ? (value !== '__proto__')
        : (value === null)
}
const getMapData = (map, key) => {
    const data = map.__data__
    return isKeyable(key)
        ? data[typeof key == 'string' ? 'string' : 'hash']
        : data.map
}

class Hash {
    constructor(entries) {
        let index = -1
        const length = entries == null ? 0 : entries.length

        this.clear()
        while (++index < length) {
            const entry = entries[index]
            this.set(entry[0], entry[1])
        }
    }
    clear() {
        this.__data__ = {}
    }
    delete(key) {
        const result = this.has(key) && delete this.__data__[key]
        this.size -= result ? 1 : 0
        return result
    }
    get(key) {
        const data = this.__data__
        return Object.prototype.hasOwnProperty.call(data, key) ? data[key] : undefined
    }
    has(key) {
        return Object.prototype.hasOwnProperty.call(this.__data__, key)
    }
    set(key, value) {
        const data = this.__data__
        this.size += this.has(key) ? 0 : 1
        data[key] = value
        return this
    }
}

class MapCache {
    constructor(entries) {
        let index = -1
        const length = entries == null ? 0 : entries.length

        this.clear()
        while (++index < length) {
            const entry = entries[index]
            this.set(entry[0], entry[1])
        }
    }
    clear() {
        this.size = 0
        this.__data__ = {
            'hash': new Hash,
            'map': new Map,
            'string': new Hash
        }
    }
    delete(key) {
        const result = getMapData(this, key).delete(key)
        this.size -= result ? 1 : 0
        return result
    }
    get(key) {
        return getMapData(this, key).get(key)
    }
    has(key) {
        return getMapData(this, key).has(key)
    }
    set(key, value) {
        const data = getMapData(this, key),
            size = data.size

        data.set(key, value)
        this.size += data.size === size ? 0 : 1
        return this
    }
}

class Stack {
    constructor(entries) {
        this.__data__ = new ListCache(entries)
        this.size = this.__data__.size
    }
    clear() {
        this.__data__ = new ListCache
        this.size = 0
    }
    delete(key) {
        const data = this.__data__,
            result = data.delete(key)

        this.size = data.size
        return result
    }
    get(key) {
        return this.__data__.get(key)
    }
    has(key) {
        return this.__data__.has(key)
    }
    set(key, value) {
        let data = this.__data__
        if (data instanceof ListCache) {
            const pairs = data.__data__
            data = this.__data__ = new MapCache(pairs)
        }
        data.set(key, value)
        this.size = data.size
        return this
    }
}

const isObjectLike = (value) => (value != null && typeof value == 'object')

const isSet = (value) => isObjectLike(value) && getTag(value) == setTag

const isMap = (value) => isObjectLike(value) && getTag(value) == mapTag

const isLength = (value) => typeof value == 'number' && value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER

const isFunction = (value) => {
    if (!isObject(value)) return false
    const tag = getTag(value)
    return tag == funcTag || tag == genTag || tag == asyncTag || tag == proxyTag
}

const isArrayLike = (value) => value != null && isLength(value.length) && !isFunction(value)

const isArguments = (value) => {
    const argsTag = '[object Arguments]'
    return isObjectLike(value) && getTag(value) == argsTag
    // return isObjectLike(value)
    //     && Object.prototype.hasOwnProperty.call(value, 'callee')
    //     && Object.prototype.propertyIsEnumerable.call(value, 'callee')
}

const isPrototype = (value) => {
    const Ctor = value && value.constructor,
        proto = (typeof Ctor == 'function' && Ctor.prototype) || objectProto

    return value === proto
}

const baseTimes = (n, iteratee) => {
    const result = Array(n)
    let index = -1
    while (++index < n) {
        result[index] = iteratee(index)
    }
    return result
}

const isIndex = (value, length) => {
    const type = typeof value
    length = length == null ? MAX_SAFE_INTEGER : length

    return !!length && (
        type == 'number' || (
            type != 'symbol' && (
                value > -1 && value % 1 == 0 && value < length
            )
        )
    )
}

const arrayLikeKeys = (value, inherited) => {
    const isArr = Array.isArray(value),
        isArg = !isArr && isArguments(value),
        skipIndexes = isArr || isArg,
        result = skipIndexes ? baseTimes(value.length, String) : [],
        length = result.length

    for (let key in value) {
        if ((inherited || Object.prototype.hasOwnProperty.call(value, key)) && !(
            skipIndexes && (
                key == 'length' ||
                isIndex(key, length)
            )
        )) {
            result.push(key)
        }
    }
    return result
}

const baseKeys = (object) => {
    if (!isPrototype(object)) return Object.keys(Object(object))

    const result = []
    for (let key in Object(object)) {
        if (Object.prototype.hasOwnProperty.call(object, key) && key != 'constructor') {
            result.push(key)
        }
        return result
    }
}

const arrayFilter = (array, predicate) => {
    let index = -1,
        resIndex = 0
    const length = array == null ? 0 : array.length,
        result = []

    while (++index < length) {
        var value = array[index]
        if (predicate(value, index, array)) {
            result[resIndex++] = value
        }
    }
    return result
}

const getSymbols = (object) => {
    if (object == null) return []
    object = Object(object)
    return arrayFilter(Object.getOwnPropertySymbols(object), (symbol) => Object.prototype.propertyIsEnumerable.call(object, symbo))
}

const arrayPush = (array, values) => {
    let index = -1
    const length = values.length,
        offset = array.length

    while (++index < length) {
        array[offset + index] = values[index]
    }
    return array
}

const keysFunc = (object) => {
    const result = isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object)
    return Array.isArray(object) ? result : arrayPush(result, getSymbols(object))
}

const arrayEach = (array, iteratee) => {
    let index = -1
    const length = array == null ? 0 : array.length

    while (++index < length) {
        if (iteratee(array[index], index, array) === false) {
            break
        }
    }
    return array
}

const assignValue = (object, key, value) => {
    const objValue = object[key]
    if (!(Object.prototype.hasOwnProperty.call(object, key) && eq(objValue, value)) ||
        (value === undefined && !(key in object))) {
        if (key == '__proto__' && defineProperty) {
            defineProperty(object, key, {
                'configurable': true,
                'enumerable': true,
                'value': value,
                'writable': true
            })
        } else {
            object[key] = value
        }
    }
}

export const cloneDeep = (value, stack) => {
    let result
    if (!isObject(value)) {
        return value
    }
    const isArr = Array.isArray(value)
    if (isArr) {
        result = initCloneArray(value)
    } else {
        const tag = getTag(value)
        const isFunc = tag === funcTag || tag === genTag

        if (tag === objectTag || tag === argsTag || isFunc) {
            result = isFunc ? {} : initCloneObject(value)
        } else {
            if (!cloneableTags[tag]) {
                return {}
            }
            result = initCloneByTag(value, tag)
        }
    }

    // 检查循环渲染
    stack || (stack = new Stack)
    const stacked = stack.get(value)

    if (stacked) {
        return stacked
    }
    stack.set(value, result)
    if (isSet(value)) {
        value.forEach((subValue) => {
            result.add(cloneDeep(subValue, stack))
        })
    } else if (isMap(value)) {
        value.forEach((subValue, key) => {
            result.set(key, cloneDeep(subValue, stack))
        })
    }

    const props = isArr ? undefined : keysFunc(value)

    arrayEach(props || value, (subValue, key) => {
        if (props) {
            key = subValue
            subValue = value[key]
        }
        // 递归填充克隆(容易受到调用堆栈限制)
        assignValue(result, key, cloneDeep(subValue, stack))
    })
    return result
}
cloneDeep

console结果可能不准确,按F12打开控制台查看