从一道深拷贝面试题中学到的知识记录


以前心里一直很抵触刷题,不过最近看了不少面试题才发现通过这些题反倒是有不少收获,有空看看就当是夯实基础吧 ~

在理解深/浅拷贝之前先弄明白对象赋值。

当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。

let obj = {
    a: 1,
    b: 'ha',
    c: {
        d: true
    }
}
let obj0 = obj
obj0.b = 'haha'
obj0.c.d = false
obj0.c.e = 2
console.log('obj', obj)
console.log('obj0', obj0)

// obj {a: 1, b: 'haha', c: {d: false, e: 2}}
// obj0 {a: 1, b: 'haha', c: {d: false, e: 2}}

深拷贝和浅拷贝的定义

浅拷贝

创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址,所以如果其中一个对象改变了这个地址,就会影响到另一个对象

浅拷贝.png

深拷贝

将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

对于上面的例子

深拷贝.png

实现浅拷贝

function shallowClone (obj) {
    var result = {}
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            result[key] = obj[key]
        }
    }
    return result
}
let obj = {
    a: 1,
    b: 'ha',
    c: {
        d: true
    }
}
let obj1 = shallowClone(obj)
obj1.b = 'haha'
obj1.c.d = false
obj1.c.e = 2
console.log('obj', obj)
console.log('obj1', obj1)

// obj {a: 1, b: 'ha', c: {d: false, e: 2}}
// obj1 {a: 1, b: 'haha', c: {d: false, e: 2}}

实现深拷贝

用JSON

const obj3 = JSON.parse(JSON.stringify(obj))

缺点:

  • 不支持 Date、正则、undefined、函数等数据(记忆点:JSON存储的数据类型)

  • 不支持引用(环状结构,obj.self=obj

用递归

关于实现深拷贝代码随便一搜就有了,但一大串代码本小白看得云里雾里,下面一步一步解释,虽繁琐,但有助于理解。

1. 判断基本类型 | 引用类型

function deepClone (obj) {
    if (obj instanceof Object) {
        // 这里先空着,接下来要针对类做判断
    } else {
        return obj
    }
}

2. 判断类(函数 | 数组 | 正则 | 日期 | 普通对象)

function deepClone (obj) {
    if (obj instanceof Object) {
        let result  // 深拷贝结果
        if (obj instanceof Function) {
            // 这里先空着,接下来要针对函数类型做判断
        } else if (obj instanceof Array) {
            result = [] // 或者 result = new Array()
        } else if (obj instanceof RegExp) {
            result= new RegExp(obj) // 或者 result= new RegExp(obj.source, obj.flags)
        } else if (obj instanceof Date) {
            result = new Date(obj - 0)   // 日期类型-0变成时间戳
        } else {
            result = {}
        }
        for (let key in obj) {
            result[key] = obj[key]
        }
        return result
    } else {
        return obj
    }
}

3. 判断普通函数 | 箭头函数

function deepClone (obj) {
    if (obj instanceof Object) {
        let result  // 深拷贝结果
        if (obj instanceof Function) {
            if (obj.prototype) {    // 有 prototype 就是普通函数
                result = function () {
                    return obj.apply(this, arguments)
                }
            } else {
                result = (...args) => obj.call(undefined, ...args)
            }
        } else if (obj instanceof Array) {
            result = [] // 或者 result = new Array()
        } else if (obj instanceof RegExp) {
            result= new RegExp(obj) // 或者 result= new RegExp(obj.source, obj.flags)
        } else if (obj instanceof Date) {
            result = new Date(obj - 0)   // 日期类型-0变成时间戳
        } else {
            result = {}
        }
        for (let key in obj) {
            result[key] = obj[key]  // 这里只考虑到第一层属性
        }
        return result
    } else {
        return obj
    }
}

4. 考虑更深层属性(递归)

function deepClone (obj) {
    if (obj instanceof Object) {
        let result  // 深拷贝结果
        if (obj instanceof Function) {
            if (obj.prototype) {    // 有 prototype 就是普通函数
                result = function () {
                    return obj.apply(this, arguments)
                }
            } else {
                result = (...args) => obj.call(undefined, ...args)
            }
        } else if (obj instanceof Array) {
            result = [] // 或者 result = new Array()
        } else if (obj instanceof RegExp) {
            result= new RegExp(obj) // 或者 result= new RegExp(obj.source, obj.flags)
        } else if (obj instanceof Date) {
            result = new Date(obj - 0)   // 日期类型-0变成时间戳
        } else {
            result = {}
        }
        for (let key in obj) {
            result[key] = deepClone(obj[key])
        }
        return result
    } else {
        return obj
    }
}

5. 考虑环状引用(obj.self=obj

let map = new Map() // 后面回答为什么要用 Map
function deepClone (obj) {
    if (map.get(obj)) {
        return map.get(obj)
    }
    if (obj instanceof Object) {
        let result  // 深拷贝结果
        if (obj instanceof Function) {
            if (obj.prototype) {    // 有 prototype 就是普通函数
                result = function () {
                    return obj.apply(this, arguments)
                }
            } else {
                result = (...args) => obj.call(undefined, ...args)
            }
        } else if (obj instanceof Array) {
            result = [] // 或者 result = new Array()
        } else if (obj instanceof RegExp) {
            result= new RegExp(obj) // 或者 result= new RegExp(obj.source, obj.flags)
        } else if (obj instanceof Date) {
            result = new Date(obj - 0)   // 日期类型-0变成时间戳
        } else {
            result = {}
        }
        map.set(obj, result)
        for (let key in obj) {
            result[key] = deepClone(obj[key])
        }
        return result
    } else {
        return obj
    }
}

环状引用即对象的属性间接或直接的引用了自身导致递归进入死循环导致栈内存溢出,所以需要另外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝。那么这个存储空间的数据格式应该为 key-value 形式,并且 key 可能是引用类型,所以选择 Map 这种数据结构。

6. 考虑 map 应该在哪声明

let obj = {
    a: 1,
    b: 'ha',
    c: {
        d: true
    }
}
let obj1 = deepClone(obj)
let obj2 = deepClone(obj)
obj1.self = obj1
console.log('obj1',obj1)
console.log('obj2',obj2)

运行后会发现 obj2 也有 self 属性,所以 map 不该声明为全局变量

function deepClone (obj, map = new Map()) {
    if (map.get(obj)) {
        return map.get(obj)
    }
    if (obj instanceof Object) {
        let result  // 深拷贝结果
        if (obj instanceof Function) {
            if (obj.prototype) {    // 有 prototype 就是普通函数
                result = function () {
                    return obj.apply(this, arguments)
                }
            } else {
                result = (...args) => obj.call(undefined, ...args)
            }
        } else if (obj instanceof Array) {
            result = [] // 或者 result = new Array()
        } else if (obj instanceof RegExp) {
            result= new RegExp(obj) // 或者 result= new RegExp(obj.source, obj.flags)
        } else if (obj instanceof Date) {
            result = new Date(obj - 0)   // 日期类型-0变成时间戳
        } else {
            result = {}
        }
        map.set(obj, result)
        for (let key in obj) {
            result[key] = deepClone(obj[key], map)
        }
        return result
    } else {
        return obj
    }
}

7. 考虑拷贝应该是拷贝自身属性

function deepClone (obj, map = new Map()) {
    if (map.get(obj)) {
        return map.get(obj)
    }
    if (obj instanceof Object) {
        let result  // 深拷贝结果
        if (obj instanceof Function) {
            if (obj.prototype) {    // 有 prototype 就是普通函数
                result = function () {
                    return obj.apply(this, arguments)
                }
            } else {
                result = (...args) => obj.call(undefined, ...args)
            }
        } else if (obj instanceof Array) {
            result = [] // 或者 result = new Array()
        } else if (obj instanceof RegExp) {
            result= new RegExp(obj) // 或者 result= new RegExp(obj.source, obj.flags)
        } else if (obj instanceof Date) {
            result = new Date(obj - 0)   // 日期类型-0变成时间戳
        } else {
            result = {}
        }
        map.set(obj, result)
        for (let key in obj) {
            if (obj.hasOwnProperty(key)) {
                result[key] = deepClone(obj[key], map)
            }
        }
        return result
    } else {
        return obj
    }
}

至此算是基本结束了,一个完整的深拷贝应当还要考虑更多的情况(如跨 iframe),这里只是实现了一些基本的功能,不过在这个推理过程我受益良多:

  • 判断数据类型和对象类型

  • 全局变量的影响

  • Map 用法

  • 普通函数和箭头函数区别

  • 递归思维

  • 考虑问题应当严谨,既要向前多走一步,还要往后多看一眼

使用 lodash 库

_.clone 方法

var objects = [{ 'a': 1 }, { 'b': 2 }];
 
var shallow = _.clone(objects);
console.log(shallow[0] === objects[0]);
// => true

_.cloneDeep 方法

var objects = [{ 'a': 1 }, { 'b': 2 }];
 
var deep = _.cloneDeep(objects);
console.log(deep[0] === objects[0]);
// => false

文章作者: April-cl
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 April-cl !
  目录