【实习周记01】完美实现腾讯面试官的深拷贝

435 阅读4分钟

Header

前几天为了暑期实习又和腾讯的面试官对线了一波,大佬上来直接让我手写一个深拷贝,这不就是想考察我对于JS数据类型和递归算法的一些理解嘛。虽然自己没特意写过,但大概还是知道一些思路什么的,用递归和类型判断稍微写了个简单的深拷贝。

接下来面试官就开始了例行优化,我也按照他的一些建议在结束后优化了下,其中还是有很多值得去学习的点,下面内容就是一个逐步优化的过程。




Content

当时写的是一个比较简陋的版本,今天试着把它写得尽量完善点。关于用Object.assign()或者json序列化的什么优缺点啥的本文不再赘述,大家不应该不会吧~(阴阳怪气)


version 1.0

/**
 * @param {*} obj 需要拷贝的数据
 * @return {*} 拷贝之后的数据
 **/
var cloneObj = function (obj) {
    // 先判断是不是null和基本数据类型
    if (obj === null || typeof obj !== 'object') {
        return obj
    }
    // 然后判断数组和对象,递归赋值就行
    let result = Array.isArray(obj) ? [] : {}
    for (let item in obj) {
        result[item] = cloneObj(obj[item])
    }
    return result
}

这个版本就是非常基础的版本,里面其实还有很多很多细节需要完善,因为传的是个对象,对象里面可能有一些正则或者函数,或者是拷贝循环引用的时候报错,又或者是ES6中的一些复杂类型的数据结构:SetWeakSetMapWeakMap,接下来我们就一步步地完善。




version 2.0

我们先来解决循环调用的问题,循环调用就像是套娃,对象一直调用自己,导致函数也一直递归调用,最终内存溢出报错。


var obj = {a: 1}
obj.b = obj
cloneObj(obj)

这个解决思路的话就是保存参数作为判断嘛,可以用WeakMap把之前遍历的值都存储下来,如果有相同的,那么就直接返回对象,不去递归了。这种场景下WeakMapMap存键值对更加好一点,可以节省内存。


/**
 * @param {*} obj 需要拷贝的数据
 * @return {*} 拷贝之后的数据
 **/
var wm = new WeakMap()
var cloneObj = function (obj) {
    // 先判断是不是null和基本数据类型
    if (obj === null || typeof obj !== 'object') {
        return obj
    }
    let result = Array.isArray(obj) ? [] : {}
    // 判断该值是否在WeakMap中
    let existObj = wm.get(obj)
    if (existObj) {
        return existObj
    }
    wm.set(obj, result)
    // 然后判断数组和对象,递归赋值就行
    for (let item in obj) {
        result[item] = cloneObj(obj[item])
    }
    return result
}

这样的话就成功解决循环引用的问题了,亲测chrome调试台能够拷贝。当然还有就是objectDateRegExpFunctionMath这些对象的处理了,这时候就需要ES5判断类型的方法了。




version 3.0

/**
 * @param {*} obj 需要拷贝的数据
 * @return {*} 拷贝之后的数据
 **/
var wm = new WeakMap()
var cloneObj = function (obj) {
    // 先判断是不是null和基本数据类型或者其他对象类型
    if (obj === null || typeof obj !== 'object' ||  Object.prototype.toString.call(obj) !== "[object Object]") {
        return obj
    } 
    let result = Array.isArray(obj) ? [] : {}
    // 判断该值是否在WeakMap中
    let existObj = wm.get(obj)
    if (existObj) {
        return existObj
    }
    wm.set(obj, result)
    // 然后判断数组和对象,递归赋值就行
    for (let item in obj) {
        result[item] = cloneObj(obj[item])
    }
    return result
}

亲测chrome调试台能够拷贝除了Math对象以外的任何对象,有个特例这怎么能忍,这可是完美copy,赶紧瞅了瞅Math对象的数据结构,发现原来里面内嵌了很多方法,那就直接再做一轮判断。尝试了一波,发现好像还是拷贝的引用......

算了我忍了,能想到的方法都试了一遍,还是只能拷贝一个引用,可能这就是内置对象的魅力吧。




version 4.0

目前还有就是SetWeakSetMapWeakMap这四种复杂类型数据结构的拷贝,目前的思路还是用ES5的方法去判断类型,然后再执行相应逻辑。像这种键值对的数据结构就得换一种赋值操作了。


/**
 * @param {*} obj 需要拷贝的数据
 * @return {*} 拷贝之后的数据
 **/
var wm = new WeakMap()
var cloneObj = function (obj) {
    // 先判断是不是null和基本数据类型或者其他对象类型
    if (obj === null || typeof obj !== 'object') {
        return obj
    } 
    // 判断数据类型
    let type = Object.prototype.toString.call(obj)
    type = type.slice(8, type.length - 1);
    let result = new window[type]();
    // 判断是不是特殊对象
    if (['Funcition', 'RegExp', 'Date'].includes(type)) {
        return obj
    }
    // 判断该值是否在WeakMap中
    let existObj = wm.get(obj)
    if (existObj) {
        return existObj
    }
    wm.set(obj, result)
    // 然后判断复杂数据类型和简单数据类型
    if (['Map', 'WeakMap'].includes(type)) {
        obj.forEach((value, key) => {
            result.set(key, cloneObj(value));
        })
    } else if (['Set', 'WeakSet'].includes(type)){
        obj.forEach((value) => {
            result.add(cloneObj(value));
        })    
    } else {
        for (let item in obj) {
            result[item] = cloneObj(obj[item])
        }
    }
    return result
}




Footer

写到这里,其实还是有很多细节没有处理,比如symbol类型的,还有就是函数的性能问题等等,其实应该还有更好的优化方案,如果有的话,欢迎各位大佬留言告知。先放在这,后期再继续优化。。


表情.png

如果这篇文章对你有帮助的话,欢迎点赞关注转发,最起码点个赞吧(脸皮真厚嘻嘻)