面试官:手写一个惊艳的深拷贝

171 阅读3分钟

快速实现

const myClone1 = (target) => {
    return JSON.parse(JSON.stringify(target))
}

优点:简单明了,适合大部分场景

缺点:JSON.stringify方法的局限性,拷贝函数等类型有问题,可参考stringify的详解

基础版

浅拷贝

const myClone2 = (target) => {
    const cloneTarget = {}
    for (const key in target) {
        cloneTarget[key] = target[key]
    }
    return cloneTarget
}

深拷贝

const myClone3 = (target) => {
    if (typeof target === 'object') {
        const cloneTarget = {}
        for (const key in target) {
            // 关键差异:递归实现深拷贝
            cloneTarget[key] = myClone3(target[key])
        }
        return cloneTarget
    } else {
        return target
    }
}

有了以上深拷贝的雏形,我们再继续探索更多特殊情况

考虑数组

const myClone4 = (target) => {
    if (typeof target === 'object') {
        // 关键差异:考虑数组情况
        const cloneTarget = Array.isArray(target) ? [] : {}
        for (const key in target) {
            cloneTarget[key] = myClone4(target[key])
        }
        return cloneTarget
    } else {
        return target
    }
}

以上我们的深拷贝方法已经可以处理多种情况,但是如果出现循环引用,递归进入死循环:

const x = {}
const y = {a: x}
x.y = y
myClone4(x) // Uncaught RangeError: Maximum call stack size exceeded

考虑循环引用

处理循环引用问题,我们只需要通过一个map来标记,如果存在对应target的key已经克隆过,就中断递归返回结果

const myClone5 = (target, map = new Map()) => {
    if (typeof target === 'object') {
        const cloneTarget = Array.isArray(target) ? [] : {}
        
        // 关键差异:map标记key,如果有值则中断递归
        if (map.get(target)) {
            return map.get(target)
        }
        map.set(target, cloneTarget)
        
        for (const key in target) {
            cloneTarget[key] = myClone5(target[key], map)
        }
        return cloneTarget
    } else {
        return target
    }
}

这里之所以用map而不用普通对象来缓存,主要考虑到键/值对中的键,这里是对象,使用map更方便

这里又有一个优化的点,这里的键只是用来做key的标记,可以使用WeakMap来替换Map

WeakMap对象中的键是弱引用,其键必须是对象,而值可以是任意的

弱引用:在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收

const myClone5 = (target, map = new WeakMap()) => { // 替换Map为WeakMap
    if (typeof target === 'object') {
        const cloneTarget = Array.isArray(target) ? [] : {}
        
        if (map.get(target)) {
            return map.get(target)
        }
        map.set(target, cloneTarget)
        
        for (const key in target) {
            cloneTarget[key] = myClone5(target[key], map)
        }
        return cloneTarget
    } else {
        return target
    }
}

考虑其他类型

除了上面提到的普通对象和数组,还需要考虑Map、Set等情况,我们需要使用toString获取准确的引用类型

const G_TAG_TYPE = {
    mapTag: '[object Map]',
    weakMapTag: '[object WeakMap]',
    setTag: '[object Set]',
    arrayTag: '[object Array]',
    objectTag: '[object Object]',
    boolTag: '[object Boolean]',
    dateTag: '[object Date]',
    errorTag: '[object Error]',
    numberTag: '[object Number]',
    regexpTag: '[object RegExp]',
    stringTag: '[object String]',
    symbolTag: '[object Symbol]'
}

// 可以继续用myClone的类型
const G_DEEP_TAG = [G_TAG_TYPE.mapTag,  G_TAG_TYPE.weakMapTag, G_TAG_TYPE.setTag, G_TAG_TYPE.arrayTag, G_TAG_TYPE.objectTag]

// Bool、Number、String、String、Date、Error、RegExp直接用构造函数和原始数据创建一个新对象:
const cloneOtherType = (target, type) => {
    switch (type) {
        case G_TAG_TYPE.boolTag:
        case G_TAG_TYPE.numberTag:
        case G_TAG_TYPE.stringTag:
        case G_TAG_TYPE.errorTag:
        case G_TAG_TYPE.dateTag:
            return new target.constructor(target)
        case G_TAG_TYPE.regexpTag: {
            const reFlags = /\w*$/
            const result = new target.constructor(target.source, reFlags.exec(target)); 
            result.lastIndex = target.lastIndex;
            return result
        }
        case G_TAG_TYPE.symbolTag:
            return Object(Symbol.prototype.valueOf.call(target))
    }
}

// null处理、函数兼容
const isObject = (target) => {
    const type = typeof target
    return target !== null && (type === 'object' || type === 'function')
}

const myClone6 = (target, map = new WeakMap()) => { // 替换Map为WeakMap
    if (!isObject(target)) {
        return target
    }
    const type = Object.prototype.toString.call(target)
    let cloneTarget
    
    if (G_DEEP_TAG.includes(type)) {
        cloneTarget = new target.constructor()
    } else {
        return cloneOtherType(target, type)
    }
    
    if (map.get(target)) {
        return map.get(target)
    }
    map.set(target, cloneTarget)
    
    // 克隆set
    if (type === G_TAG_TYPE.setTag) {
        target.forEach(v => {
            cloneTarget.add(myClone6(v, map))
        })
        return cloneTarget
    }

    // 克隆map和weakMap
    if (type === G_TAG_TYPE.mapTag || type === G_TAG_TYPE.weakMapTag) {
        target.forEach((v, key) => {
            cloneTarget.set(key, myClone6(v, map))
        })
        return cloneTarget
    }

    for (const key in target) {
        cloneTarget[key] = myClone6(target[key], map)
    }
    return cloneTarget
}

克隆函数

最后我们再考虑克隆函数

  1. 首先我们可以通过箭头函数没有prototype的特性,来区分箭头函数和普通函数
  2. 使用eval和函数字符串来重新生成一个箭头函数,注意这种方法是不适用于普通函数的
  3. 使用正则来处理普通函数,然后使用new Function构造函数重新构造一个新的函数
const cloneFunction = (func) => {
    const bodyReg = /(?<={)(.|\n)+(?=})/m
    const paramReg = /(?<=\().+(?=\)\s+{)/
    const funcString = func.toString()

    if (func.prototype) {
        const param = paramReg.exec(funcString)
        const body = bodyReg.exec(funcString)
        if (body) {
            if (param) {
                const paramArr = param[0].split(',')
                return new Function(...paramArr, body[0])
            } else {
                return new Function(body[0])
            }
        } else {
            return null
        }
    } else {
        return eval(funcString)
    }
}

完整深拷贝函数

const G_TAG_TYPE = {
    mapTag: '[object Map]',
    weakMapTag: '[object WeakMap]',
    setTag: '[object Set]',
    arrayTag: '[object Array]',
    objectTag: '[object Object]',
    boolTag: '[object Boolean]',
    dateTag: '[object Date]',
    errorTag: '[object Error]',
    numberTag: '[object Number]',
    regexpTag: '[object RegExp]',
    stringTag: '[object String]',
    symbolTag: '[object Symbol]',
    functionTag: '[object Function]'
}

// 可以继续用myClone的类型
const G_DEEP_TAG = [G_TAG_TYPE.mapTag, G_TAG_TYPE.weakMapTag, G_TAG_TYPE.setTag, G_TAG_TYPE.arrayTag, G_TAG_TYPE.objectTag]

// Bool、Number、String、String、Date、Error、RegExp直接用构造函数和原始数据创建一个新对象:
const cloneOtherType = (target, type) => {
    switch (type) {
        case G_TAG_TYPE.boolTag:
        case G_TAG_TYPE.numberTag:
        case G_TAG_TYPE.stringTag:
        case G_TAG_TYPE.errorTag:
        case G_TAG_TYPE.dateTag:
            return new target.constructor(target)
        case G_TAG_TYPE.regexpTag: {
            const reFlags = /\w*$/
            const result = new target.constructor(target.source, reFlags.exec(target)); 
            result.lastIndex = target.lastIndex;
            return result
        }
        case G_TAG_TYPE.symbolTag:
            return Object(Symbol.prototype.valueOf.call(target))
        case G_TAG_TYPE.functionTag:
            return cloneFunction(target)
    }
}

const cloneFunction = (func) => {
    const bodyReg = /(?<={)(.|\n)+(?=})/m
    const paramReg = /(?<=\().+(?=\)\s+{)/
    const funcString = func.toString()

    if (func.prototype) {
        const param = paramReg.exec(funcString)
        const body = bodyReg.exec(funcString)
        if (body) {
            if (param) {
                const paramArr = param[0].split(',')
                return new Function(...paramArr, body[0])
            } else {
                return new Function(body[0])
            }
        } else {
            return null
        }
    } else {
        return eval(funcString)
    }
}

// null处理、函数兼容
const isObject = (target) => {
    const type = typeof target
    return target !== null && (type === 'object' || type === 'function')
}

const myClone7 = (target, map = new WeakMap()) => { // 替换Map为WeakMap
    if (!isObject(target)) {
        return target
    }
    const type = Object.prototype.toString.call(target)
    let cloneTarget
    
    if (G_DEEP_TAG.includes(type)) {
        cloneTarget = new target.constructor()
    } else {
        return cloneOtherType(target, type)
    }
    
    if (map.get(target)) {
        return map.get(target)
    }
    map.set(target, cloneTarget)
    
    // 克隆set
    if (type === G_TAG_TYPE.setTag) {
        target.forEach(v => {
            cloneTarget.add(myClone7(v, map))
        })
        return cloneTarget
    }

    // 克隆map和weakMap
    if (type === G_TAG_TYPE.mapTag || type === G_TAG_TYPE.weakMapTag) {
        target.forEach((v, key) => {
            cloneTarget.set(key, myClone7(v, map))
        })
        return cloneTarget
    }

    for (const key in target) {
        cloneTarget[key] = myClone7(target[key], map)
    }
    return cloneTarget
}

参考文章

如何写出一个惊艳面试官的深拷贝?

有意思的JSON.parse、JSON.stringify