手写深拷贝

126 阅读3分钟

手写深拷贝

JS基础知识:变量的类型和计算

注意: Object.assign 不是深拷贝!只拷贝了一层 以下为简易版

/**
 * 深拷贝
 */
const obj1 = {
    age: 20,
    name: 'xxx',
    address: {
        city: 'beijing'
    },
    arr: ['a', 'b', 'c']
}

const obj2 = deepClone(obj1)
obj2.address.city = 'shanghai'
obj2.arr[0] = 'a1'
console.log(obj1.address.city)
console.log(obj1.arr[0])

/**
 * 深拷贝
 * @param {Object} obj 要拷贝的对象
 */
function deepClone(obj = {}) {
    if (typeof obj !== 'object' || obj == null) {
        // obj 是 null ,或者不是对象和数组,直接返回
        return obj
    }

    // 初始化返回结果
    let result
    if (obj instanceof Array) {
        result = []
    } else {
        result = {}
    }

    for (let key in obj) {
        // 保证 key 不是原型的属性
        if (obj.hasOwnProperty(key)) {
            // 递归调用!!!
            result[key] = deepClone(obj[key])
        }
    }

    // 返回结果
    return result
}

实现一个完整的深拷贝如下:

typeof 运算符

  • 识别所有值类型
  • 识别函数
  • 判断是否是引用类型(不可再细分)
  • typeof null === 'object'

JSON.stringfy() 实现深拷贝存在的问题:

  1. 执行会报错:存在BigInt类型、循环引用。
  2. 拷贝Date引用类型会变成字符串。
  3. 键值会消失:对象的值中为Function、Undefined、Symbol 这几种类型,。
  4. 键值变成空对象:对象的值中为Map、Set、RegExp这几种类型。
  5. 无法拷贝:不可枚举属性、对象的原型链。
  6. 补充:其他更详细的内容请查看官方文档:JSON.stringify() - JavaScript | MDN (mozilla.org)

深拷贝需要考虑的问题:

  • 基本类型数据是否能拷贝?
  • 键和值都是基本类型的普通对象是否能拷贝?
  • Symbol作为对象的key是否能拷贝?
  • Date和RegExp对象类型是否能拷贝?
  • Map和Set对象类型是否能拷贝?
  • Function对象类型是否能拷贝?(函数我们一般不用深拷贝)
  • 对象的原型是否能拷贝?
  • 不可枚举属性是否能拷贝?
  • 循环引用是否能拷贝?

完整版实现:

JavaScript深拷贝看这篇就行了!(实现完美的ES6+版本)码飞CC的博客-CSDN博客

 * 深拷贝完整版实现
 * @param {Object} target 
 * @returns 
 */
function deepClone(target = {}) {
    // WeakMap作为记录对象的hash表 (用于防止循环引用)
    const map = new WeakMap()

    // 工具函数
    function isObject(target) {
        return (typeof target === 'object') || typeof target === 'function'
    }

    // 拷贝主逻辑
    function clone(data) {

        // 基础类型直接返回
        if (!isObject(data)) {
            return data
        }

        // 日期或者正则对象,则直接构造一个新的对象返回
        if ([Date, RegExp].includes(data.constructor)) {
            return new data.constructor(data)
        }

        // 处理函数对象
        if (typeof data === 'function') {
            return new Function('retuen' + data.toString())()
        }

        // 如果对象已经存在,则直接返回该对象
        const exist = map.get(data)
        if (exist) {
            return exist
        }

        // 处理 Map 对象
        if (data instanceof Map) {
            const result = new Map()
            map.set(data, result)
            data.forEach((val, key) => {
                // 注意:map种的值为 object 的话也得深拷贝
                if (isObject(val)) {
                    result.set(key.clone(val))
                } else {
                    result.set(key, val)
                }
            })
            return result
        }

        // 处理 Set 对象
        if (data instanceof Set) {
            const res = new Set()
            map.set(data, res)
            data.forEach(val => {
                if (isObject(val)) {
                    res.add(clone(val))
                } else {
                    res.add(val)
                }
            })
            return res
        }

        // 收集键名(考虑以Symbol作为key以及不可枚举的属性)
        // Reflect.ownKeys(obj)相当于[...Object.getOwnPropertyNames(obj), ...Object.getOwnPropertySymbols(obj)]
        const keys = Reflect.ownKeys(data)
        // 获得对象的所有属性以及对应的属性描述
        const allDesc = Object.getOwnPropertyDescriptors(data)
        // 创建一个新对象, 继承传入对象的原型链、(浅拷贝)
        const result = Object.create(Object.getPrototypeOf(data), allDesc)

        // 新对象加入map中,进行记录
        map.set(data, result)

        // Object.create() 是浅拷贝,所以要判断值的类型并递归进行深拷贝
        keys.forEach(key => {
            const val = data[key]
            if (isObject(key)) {
                result[key] = clone(val)
            } else {
                result[key] = val
            }
        })
        return result
    }

    return clone(data)
}