面试官:你会写深拷贝吗

175 阅读3分钟

前言

对于前端同学来说,对象的深拷贝可以说是面试中最火热的题目之一了,今天我们一起来把它盘明白。

先来理解下对象深拷贝的概念:

深拷贝是指创建一个对象,这个对象的内容和原始对象完全相同,但它们是存储在不同的内存地址上的,这意味着,我们修改新对象,原始对象不受影响

其中有两个关键点:

  • 对象是以keyvalue键值对的方式存储的,所以要拷贝它们必须要用循环
  • 既然要深拷贝,相较于只拷贝最外层浅拷贝,就需要用递归或循环拷贝N层

对象深拷贝的方式

JSON.parse(JSON.stringify(obj))

  • JSON.stringify:将一个对象序列化成一个JSON字符串,包括嵌套的对象属性
  • JSON.parse:将一个JSON反序列化成为一个js对象

由于在内存中 JSON 字符串的地址都是独立的,和原始对象不是同一个地址,所以我们就能通过JSON.parse解析出一个新对象了。

下面看一下用这种方式实现深拷贝的优缺点:

优点:

  1. 简单易用:语法JSON.parse(JSON.stringify(obj)),用起来非常简单
  2. 跨平台:在不同平台和环境都能用
  3. 兼容性好:各大浏览器都支持

缺点:

  1. 无法处理特殊对象类型,比如函数、正则表达式、日期对象等
    • 拷贝的时候会丢失函数和undefined
    • 时间对象会变成字符串形式
    • RegExp、Error 对象会变成空对象
    • NaN、Infinity、-Infinity会变成 null
    • 等等...
  2. 无法处理循环引用,比如在一个对象中,a引用了b,b引用了c,而c又引用了a,出现这种情况调用JSON.parse(JSON.stringify(obj))会报错

借助第三方库

实现深拷贝,一般我们会借助第三方库实现,比如lodash,lodash提供了一个cloneDeep的方法实现深拷贝

const _ = require('lodash')
const obj = { a: [{ b: 2 }] }

const res = _.cloneDeep(obj)
console.log(res)
// 输出:{ a: [ { b: 2 } ] }

深拷贝的实现

深拷贝其实实现起来要写完整,还是挺复杂的,要处理函数数组正则,甚至是symbolbuffer等,但对于面试来说,我们写个简单版本就行啦。

废话不多说,直接上完整代码。

const isObj = (target) => typeof target === 'object' && target !== null
function deepClone(obj, hash = new WeakMap()) {
    if (!isObj(obj)) return obj
    if (hash.has(obj)) return has.get(obj)
    const target = new obj.constructor()
    hash.set(obj, target)
    Object.keys(obj).forEach((key) => {
        target[key] = deepClone(obj[key], hash)
    })
    return target
}
const obj = { a: [{ b: 2 }] }

const res = deepClone(obj)
console.log(res)
// 输出:{ a: [ { b: 2 } ] }

我们用一个WeakMap来处理循环引用,然后通过拿到对象引用的constructor来复制对象,这样我们就省去了判断不同对象类型这一步,会简单很多,然后forEach循环递归复制就好啦。

虽然可能有人会说用constructor比较粗糙,但这是比较简洁的写法,我们面试的时候大可不必这么较真,只要知道它的核心思路和原理就行啦!