手写 JS 深拷贝

304 阅读4分钟

深拷贝

题目

手写 JS 深拷贝

分析

这是一个很常见的问题,看似也很简单,但是如果考虑到“高质量代码”的要求,写起来还是挺麻烦的。
别说写代码,就本节所有的情况你能否考虑全面,这都不一定。

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对象类型是否能拷贝?(函数我们一般不用深拷贝)
  • 对象的原型是否能拷贝?
  • 不可枚举属性是否能拷贝?
  • 循环引用是否能拷贝?

错误答案1

使用 JSON.stringifyJSON.parse

  • 无法转换函数
  • 无法转换 Map Set
  • 无法转换循环引用

PS:其实普通对象使用 JSON API 的运算速度很快,但功能不全

错误答案2

使用 Object.assign —— 这根本就不是深拷贝,是浅拷贝 !!!

错误答案3

只考虑了普通的对象和数组

  • 无法转换 Map Set
  • 无法转换循环引用
/**
 * 深拷贝 - 只考虑了简单的数组、对象
 * @param obj obj
 */
function cloneDeep(obj: any) {
    if (typeof obj !== 'object' || obj == null ) return obj

    let result: any
    if (obj instanceof Array) {
        result = []
    } else {
        result = {}
    }

    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {            
            result[key] = cloneDeep(obj[key]) // 递归调用
        }
    }

    return result
}
// 功能测试
const a: any = {
    set: new Set([10, 20, 30]),
    map: new Map([['x', 10], ['y', 20]])
}
a.self = a
console.log( cloneDeep(a) ) // 无法处理 Map Set 和循环引用

正确答案

循环引用 Map Set 函数

for...in语句以任意顺序迭代一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性。

/**
 * 深拷贝
 * @param obj obj
 * @param map weakmap 为了避免循环引用
 */
export function cloneDeep(obj: any, map = new WeakMap()): any {
    if (typeof obj !== 'object' || obj == null ) return obj

    // 避免循环引用
    const objFromMap = map.get(obj)
    if (objFromMap) return objFromMap

    let target: any = {}
    map.set(obj, target)

    // Map
    if (obj instanceof Map) {
        target = new Map()
        obj.forEach((v, k) => {
            const v1 = cloneDeep(v, map)
            const k1 = cloneDeep(k, map)
            target.set(k1, v1)
        })
    }

    // Set
    if (obj instanceof Set) {
        target = new Set()
        obj.forEach(v => {
            const v1 = cloneDeep(v, map)
            target.add(v1)
        })
    }

    // Array
    if (obj instanceof Array) {
        target = obj.map(item => cloneDeep(item, map))
    }

    // Object
    for (const key in obj) {
        const val = obj[key]
        const val1 = cloneDeep(val, map)
        target[key] = val1
    }

    return target
}

// // 功能测试
// const a: any = {
//     set: new Set([10, 20, 30]),
//     map: new Map([['x', 10], ['y', 20]]),
//     info: {
//         city: '北京'
//     },
//     fn: () => { console.info(100) }
// }
// a.self = a
// console.log( cloneDeep(a) )

最终完整版实现:

/**
 * 深拷贝完整版实现
 * @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)
}