循序渐进的实现一个较为完整的深拷贝

本文涉及的产品
对象存储 OSS,20GB 3个月
对象存储 OSS,恶意文件检测 1000次 1年
对象存储 OSS,内容安全 1000次 1年
简介: 循序渐进的实现一个较为完整的深拷贝

简介


对象类型在赋值的过程中其实是复制了地址,从而会导致改变了一方其他也都被改变的情况,解决这种问题的方法就是利用深/浅拷贝

浅拷贝可以直接使用扩展运算符(...)或者Object.assign直接处理

而深拷贝就没有现成的函数了

在业务开发中通常使用 JSON.parse(JSON.stringify(data)) 应付大多数的深拷贝场景

但面试就一般考察使用递归去实现一个深拷贝

本文将较为详细的介绍每一种边界情况的实现方案


常见边界问题


  • 循环引用
  • 函数
  • 正则
  • 日期
  • symbol
  • 多个键值引用了同一个对象,保持拷贝后的特性一致
  • ...


1. 简单递归实现


不考虑边界问题,元素只有 值类型,obj,arr


function deepClone(obj) {
    if (!isObject(obj)) return obj
    if (Array.isArray(obj)) {
        const newObj = []
        for (const v of obj) {
            newObj.push(isObject(v) ? deepClone(v) : v)
        }
        return newObj
    }
    if (isObject(obj)) {
        const newObj = {}
        Object.keys(obj).forEach(k => {
            const v = obj[k]
            newObj[k] = isObject(v) ? deepClone(v) : v
        })
        return newObj
    }
}
const a = {
    name: 'xiaoming', id: 123131, info: {
        bd: '2020-01-01',
        cards: [{
            q: 'q',
            w: [1, 2, 3],
            e: { c: 'c' }
        }]
    }
}
console.log(JSON.stringify(deepClone(a)));


2. 解决循环引用


众所周知 使用JSON进行深拷贝是无法解决对象的循环引用,如果出现了会直接报错


可以使用哈希表来解决此问题,将已存在的对象记录下来


对上面的deepclone稍加改动


function deepClone(obj) {
    const map = new WeakMap()
    const dp = (obj) => {
        if (!isObject(obj)) return obj
        // 解决循环引用
        if (map.has(obj)) return map.get(obj)
        map.set(obj, Array.isArray(obj) ? [] : {})
        if (Array.isArray(obj)) {
            const newObj = []
            for (const v of obj) {
                newObj.push(isObject(v) ? dp(v) : v)
            }
            return newObj
        }
        if (isObject(obj)) {
            const newObj = {}
            Object.keys(obj).forEach(k => {
                const v = obj[k]
                newObj[k] = isObject(v) ? dp(v) : v
            })
            return newObj
        }
    }
    return dp(obj)
}
const b = {}, c = {}
b.next = c
c.next = b
console.log(deepClone(b)); // { next: { next: {} } }


3. 保持原对象的引用的特性


  • 将已拷贝后的对象存储起来
  • 经clone过的对象直接返回


function deepClone(obj) {
    const map = new WeakMap()
    const dp = (obj) => {
        if (!isObject(obj)) return obj
        // 已经clone过的对象直接返回
        if (map.has(obj)) return map.get(obj)
        // 解决循环引用
        map.set(obj, Array.isArray(obj) ? [] : {})
        if (Array.isArray(obj)) {
            const newObj = []
            for (const v of obj) {
                newObj.push(isObject(v) ? dp(v) : v)
            }
            // 将已拷贝后的对象存储起来
            map.set(obj, newObj)
            return newObj
        }
        if (isObject(obj)) {
            const newObj = {}
            Object.keys(obj).forEach(k => {
                const v = obj[k]
                newObj[k] = isObject(v) ? dp(v) : v
            })
            // 将已拷贝后的对象存储起来
            map.set(obj, newObj)
            return newObj
        }
    }
    return dp(obj)
}
const obj = { a: 1 }
const t3 = { a: obj, d: obj, f: { g: obj } }
const tt3 = deepClone(t3)
console.log(tt3); // { a: { a: 1 }, d: { a: 1 }, f: { g: { a: 1 } } }
console.log(tt3.a === tt3.d); // true
console.log(tt3.a === tt3.f.g); // true


4. 拷贝Symbol


这里最主要的是如何获取到到对象的Symbol键


获取对象的键的方案有以下几种


  • Reflect.ownKeys(target): 方法返回一个由目标对象自身的属性键组成的数组(包含普通键与Symbol键)
  • Object.getOwnPropertySymbols(target):返回一个给定对象自身的所有 Symbol 属性的数组
  • Object.getOwnPropertyNames(target):返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性不包括Symbol值作为名称的属性)组成的数组
  • Object.keys():返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致


综上


Reflect.ownKeys(target) 
// 等价于
Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))


再稍加改动一下我们的deepClone方法,这里我们直接采用Reflect.ownKeys(target)替代原来的Object.keys(targer)


function deepClone(obj) {
    const map = new WeakMap()
    const dp = (obj) => {
        if (!isObject(obj)) return obj
        // 已经clone过的对象直接返回
        if (map.has(obj)) return map.get(obj)
        // 解决循环引用
        map.set(obj, Array.isArray(obj) ? [] : {})
        if (Array.isArray(obj)) {
            const newObj = []
            for (const v of obj) {
                newObj.push(isObject(v) ? dp(v) : v)
            }
            // 将已拷贝后的对象存储起来
            map.set(obj, newObj)
            return newObj
        }
        if (isObject(obj)) {
            const newObj = {}
            // 使用Reflect.ownKeys替换
            Reflect.ownKeys(obj).forEach(k => {
                const v = obj[k]
                newObj[k] = isObject(v) ? dp(v) : v
            })
            // 将已拷贝后的对象存储起来
            map.set(obj, newObj)
            return newObj
        }
    }
    return dp(obj)
}
const s1 = Symbol.for('s1')
const s2 = Symbol.for('s2')
const data = {
    [s1]: {
        name: 's1',
        age: 19
    },
    [s2]: [1, 2, 'string', {
        title: s1
    }]
}
console.log(deepClone(data));
// { [Symbol(s1)]: { name: 's1', age: 19 },
//   [Symbol(s2)]: [ 1, 2, 'string', { title: Symbol(s1) } ] }


5. 拷贝特殊对象Date/RegExp


对于特殊的对象,我们可以通过以下几步去处理

  • 获取对象的构造函数
  • 判断是否是指定的特殊对象
  • 调用构造函数生成一个新的对象


实例化的对象可以通过.constructor获取到其构造函数

我们修改上面的clone方法


function deepClone(obj) {
    const map = new WeakMap()
    const dp = (obj) => {
        if (!isObject(obj)) return obj
        // 已经clone过的对象直接返回
        if (map.has(obj)) return map.get(obj)
        // 解决循环引用
        map.set(obj, Array.isArray(obj) ? [] : {})
        // 获取对象的构造函数
        const fn = obj.constructor
        // 如果是正则
        if (fn === RegExp) {
            return new RegExp(obj)
        }
        // 如果是日期
        if (fn === Date) {
            return new Date(obj.getTime())
        }
        if (Array.isArray(obj)) {
            const newObj = []
            for (const v of obj) {
                newObj.push(isObject(v) ? dp(v) : v)
            }
            // 将已拷贝后的对象存储起来
            map.set(obj, newObj)
            return newObj
        }
        if (isObject(obj)) {
            const newObj = {}
            // 使用Reflect.ownKeys替换
            Reflect.ownKeys(obj).forEach(k => {
                const v = obj[k]
                newObj[k] = isObject(v) ? dp(v) : v
            })
            // 将已拷贝后的对象存储起来
            map.set(obj, newObj)
            return newObj
        }
    }
    return dp(obj)
}
const data = {
    today: new Date(),
    reg: /^abc$/ig
}
console.log(deepClone(data)); // { today: 2020-09-01T08:45:26.907Z, reg: /^abc$/gi }


拷贝函数


函数拷贝的方案在网上收集了一下五花八门,各种奇淫技巧,下面给大家列举一下哈哈


  1. 使用eval:
  • eval(fn.toString()):只支持箭头函数
  • new Function(‘return’+fn.toString())():不能将函数及其原始作用域一起克隆


  1. fn.bind():返回的新函数无法再通过bind去改变this指向


// 我这里就简单的使用.bind
function deepClone(obj) {
    const map = new WeakMap()
    const dp = (obj) => {
        if (!isObject(obj)) return obj
        // 已经clone过的对象直接返回
        if (map.has(obj)) return map.get(obj)
        // 解决循环引用
        map.set(obj, Array.isArray(obj) ? [] : {})
        // 获取对象的构造函数
        const fn = obj.constructor
        // 如果是正则
        if (fn === RegExp) {
            return new RegExp(obj)
        }
        // 如果是日期
        if (fn === Date) {
            return new Date(obj.getTime())
        }
        // 如果是函数
        if (fn === Function) {
            return obj.bind({})
        }
        if (Array.isArray(obj)) {
            const newObj = []
            for (const v of obj) {
                newObj.push(isObject(v) ? dp(v) : v)
            }
            // 将已拷贝后的对象存储起来
            map.set(obj, newObj)
            return newObj
        }
        if (isObject(obj)) {
            const newObj = {}
            // 使用Reflect.ownKeys替换
            Reflect.ownKeys(obj).forEach(k => {
                const v = obj[k]
                newObj[k] = isObject(v) ? dp(v) : v
            })
            // 将已拷贝后的对象存储起来
            map.set(obj, newObj)
            return newObj
        }
    }
    return dp(obj)
}
const data = {
    today: new Date(),
    reg: /^abc$/ig,
    fn1: (a, b) => {
        console.log(this.today);
        console.log(a + b);
    },
    fn2: function (a, b) {
        console.log(this.reg);
        console.log(a + b);
    }
}
const newData = deepClone(data)
newData.fn1(1, 2) // undefined 3
newData.fn1.call({ today: '666' }, 1, 2) // undefined 3
newData.fn2(3, 4) // /^abc$/gi  7
newData.fn2.call({ reg: 123 }, 3, 4) // 123 7
const fn2 = newData.fn2
fn2.call({ reg: 'fn2Call' }, 2, 3) // fn2Call 5
const fn3 = fn2.bind({ reg: 'string' })
fn3(2, 3) // string 5


更详细的内容大家可以细品一下这篇文章


关于深拷贝完整实现 可以研究一下 lodash.cloneDeep源码


相关实践学习
借助OSS搭建在线教育视频课程分享网站
本教程介绍如何基于云服务器ECS和对象存储OSS,搭建一个在线教育视频课程分享网站。
相关文章
|
6月前
|
设计模式 存储 Java
深拷贝与浅拷贝,就是这么简单
深拷贝与浅拷贝,就是这么简单
|
JSON Java API
深拷贝、浅拷贝
深拷贝、浅拷贝
81 0
|
4月前
|
编译器 C++
深拷贝和浅拷贝介绍
这篇文章讨论了C++中的数据拷贝,特别是浅拷贝和深拷贝的概念。对于基本类型和简单对象,拷贝是按位复制,即浅拷贝,类似于`memcpy()`函数的效果。当类包含动态分配的内存或其他资源时,需要显式定义拷贝构造函数以实现深拷贝,确保对象间的独立性。文中通过一个自定义的变长数组类`Array`示例说明了深拷贝的必要性,并展示了不使用深拷贝可能导致的问题。通常,如果类有指针成员,大部分情况需要深拷贝;否则,浅拷贝可能就足够了。文章还提到了在创建对象时需要预处理的情况,如记录对象创建时间或计数,这也需要深拷贝。
|
6月前
什么是深拷贝和浅拷贝哇
什么是深拷贝和浅拷贝哇
|
6月前
|
JavaScript 前端开发
浅拷贝和深拷贝
浅拷贝和深拷贝
47 2
|
C++
22 C++ - 深拷贝和浅拷贝
22 C++ - 深拷贝和浅拷贝
46 0
|
编译器 C++
C++中的深拷贝和浅拷贝介绍
对于基本类型的数据以及简单的对象,它们之间的拷贝非常简单,就是按位复制内存。例如: class Base{ public: Base(): m_a(0), m_b(0){ } Base(int a, int b): m_a(a), m_b(b){ } private: int m_a; int m_b; }; int main(){ int a = 10; int b = a; //拷贝 Base obj1(10, 20);
123 0
|
前端开发
对于深拷贝与浅拷贝的理解
对于深拷贝与浅拷贝的理解
深拷贝和浅拷贝
类里面会为我们实现默认的拷贝,这个做的是值的拷贝,但是假如对象里的数据成员在堆上开辟了内存资源,如果继续浅拷贝就会导致两根指针指向同一块资源,从而产生内存泄漏问题。但是深拷贝可以解决这个问题,本文将详细介绍深拷贝与浅拷贝。