一文彻底拿捏深浅克隆

79 阅读4分钟

前言

前两天刚好去面试,刚好碰到面试官问到 如何实现深度克隆?,现在总结下思路。

我们数据是存储在内存中,学过后端到同学应该清楚,存储有两种类型

  • 值类型
  • 引用类型

值类型 在内存中储存的是值本身,存储在栈里面,引用类型 是数据的引用,它分为两块存储:引用也就是内存地址,存储在栈里面,实际数据存储在堆里面。

克隆分为 浅克隆深克隆浅克隆 实际是 “第一层拷贝”,深克隆 是递归多层拷贝。 以常规对象为例:

const obj = {
   name: 'cola',
   info: {
      age: 1
   }
}

“第一层拷贝” 只是第一层数据值的拷贝,比如:[name, info],因为 info 是 引用类型,所以对它进行拷贝实际是引用地址的拷贝,当我们修改 obj.info.age 时,会发现原有数据也会发生变化,比如:

const obj = {
   name: 'xiaoming',
   info: {
      age: 1
   }
}
const newObj = Object.assign({}, obj);
newObj.name = 'xiaohong'
newObj.info.age = 2

console.log(obj) // {name: 'xiaoming', info: { age: 2 }}

接下来我们来看看 JavaScript 中,浅克隆深克隆 的实现方式吧

浅克隆

JavaScript 中,我们常用对象有两种:普通对象和数组,我们分为看下它们的 浅克隆(值类型不需要讨论,直接赋值即可)。

针对普通对象,一般使用

  • Object.assign
  • Object.create
  • 扩展运算符

针对数组,一般使用

  • Array.prototype.slice
  • 扩展运算符
  • Array.from
  • Array.prototype.concat
  • Object.create
  • Object.assign

深克隆

提到深拷贝,你会想到几种方式呢?

本文将介绍六种实现方式

  • JSON.parse 和 JSON.stringify
  • MessageChannel
  • Notification API
  • History API
  • structuredClone
  • 自己实现或三方库

JSON.parse

JSON.parse 和 JSON.stringify 想必是经常使用的方式了,实现深克隆非常简单:

const obj = {}
const newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj)

但是这种方式存在以下缺陷:

  • 忽略 undefinedsymbolfunction,如果存在数组中,则转换为 null
  • 不能处理循环引用,如果存在会报错
  • 所有以 symbol 为键的会忽略,即使在 replacer 强制指定
  • NaN 和 Infinity 格式的数值及 null 都会被当做 null。
const obj = {
    a: {
        b: {
            c: 1,
            d: void 0,
            e: Symbol.for('symbol'),
            f: function(){},
            g: NaN
        }
    }
}
obj.a.b.h = obj

const newObj = JSON.parse(JSON.stringify(obj, (k, v) => {
    if(typeof v === 'undefined') return ''
    if(typeof v === 'function') return v.toString()
    if(typeof k === 'symbol') return v
    return v
}))

console.log(newObj)

我们可以看到,对于一些场景,我们可以使用 replacer 进行操作,但是有些场景还是无法覆盖。

MessageChannel

function deepCopy(obj) {
  return new Promise((resolve, reject) => {
    const channel = new MessageChannel();
    channel.port1.onmessage = (event) => {
      resolve(event.data);
    };
    channel.port2.postMessage(obj);
  });
}

const obj = {
    a: {
        b: {
            c: 1,
            d: void 0,
            e: Symbol.for('symbol'),
            f: function(){},
            g: NaN
        }
    }
}
obj.a.b.h = obj
// throw error
const newObj = await deepCopy(obj)
console.log(newObj)

MessageChannel 解决了 JSON.parse 部分问题,但是依然一些问题:

  • 不能拷贝函数,会报错
  • 不能拷贝Symbol,会报错
  • 解决循环引用和 undefined 问题
  • Symbol为键还是会忽略

Notification API

Notification 也可以用来深度拷贝吗?当然可以

const obj = {
    a: {
        b: {
            c: 1,
            d: void 0,
            e: Symbol.for('symbol'),
            f: function(){},
            g: NaN
        }
    }
}
obj.a.b.h = obj
const newObj = new Notification('', {data: obj, silent: true}).data;
console.log(newObj)

它的作用和 MessageChannel 一模一样

History API

用过 vue 的朋友都知道 vue-router,它的底层使用 history api 进行路由的导航,但是你有没有想过,它会和 深克隆 扯上关系

const obj = {
    a: {
        b: {
            c: 1,
            d: void 0,
            e: Symbol.for('symbol'),
            f: function(){},
            g: NaN,
            [Symbol.for('symbol')]: 'symbol'
        }
    }
}
obj.a.b.h = obj
function deepCopy(obj){
   const old = history.state
   history.replaceState(obj, document.title)
   const value = history.state
   history.replaceState(old, document.title)
   return value
}
const newObj = deepCopy(obj)
console.log(newObj)

它的作用也和 MessageChannel 一模一样

structuredClone

浏览器新增 structuredClone API,它的兼容性如下:

image.png

const obj = {
    a: {
        b: {
            c: 1,
            d: void 0,
            e: Symbol.for('symbol'),
            f: function(){},
            g: NaN,
            [Symbol.for('symbol')]: 'symbol'
        }
    }
}
obj.a.b.h = obj
const newObj = structuredClone(obj)
console.log(newObj)

它的作用也和 MessageChannel 一模一样

自定义实现

根据以上表现,自行封装 deepCopy

基础实现

function deepCopy(obj) {
  const res = Object.create(null)
  for (const [key, value] of Object.entries(obj)) {
    if (value && typeof value === 'object') {
      res[key] = deepCopy(value)
    } else {
      res[key] = value
    }
  }
  return res
}

考虑特殊类型

JavaScript 中有一些内置的对象,比如:FunctionRegExpDate 另外 ES6 新增的对象也需要考虑,比如:MapSetSymbol

function init(obj) {
  if (obj instanceof Map) {
    return new obj.constructor()
  }
  if (obj instanceof Set) {
    return new obj.constructor()
  }
  const properties = Object.getOwnPropertyDescriptors(obj)
  return Object.create(Object.getPrototypeOf(obj), properties)
}

function deepCopy(obj) {
  if (obj && typeof obj === 'object') {
    if (obj instanceof Date) return new Date(obj)
    if (obj instanceof RegExp) return new RegExp(obj)
    if (typeof obj === 'symbol') {
      return Symbol(obj.description)
    }

    let res = init(obj)
    if (obj instanceof Map) {
      obj.forEach((value, key) => {
        res.set(key, deepCopy(value))
      })
      return res
    }

    if (obj instanceof Set) {
      obj.forEach((value) => {
        res.add(deepCopy(value))
      })
      return res
    }

    for (const [key, value] of Object.entries(obj)) {
      if (value && typeof value === 'object') {
        res[key] = deepCopy(value)
      } else {
        res[key] = value
      }
    }
    return res
  }
  return obj
}

考虑边界场景

考虑循环引用和函数

function cloneFunc(func) {
  const paramsReg = /(?<=\().+(?=\)[\s+]?\{)/m
  const bodyReg = /(?<={)(.|[\n|\n\r]?)+(?=})/m
  const func_string = func.toString()
  if (func.prototype) {
    const body = bodyReg.exec(func_string)
    const params = paramsReg.exec(func_string)
    if (body) {
      if (params) {
        params = params[0].split(',')
        return new Function(...params, body[0])
      } else {
        return Function(body[0])
      }
    } else {
      return null
    }
  } else {
    return eval(func_string)
  }
}
function init(obj) {
  if (obj instanceof Map) {
    return new obj.constructor()
  }
  if (obj instanceof Set) {
    return new obj.constructor()
  }
  const properties = Object.getOwnPropertyDescriptors(obj)
  return Object.create(Object.getPrototypeOf(obj), properties)
}

function deepCopy(obj, map = new WeakMap()) {
  if (obj && typeof obj === 'object') {
    if (obj instanceof Date) return new Date(obj)
    if (obj instanceof RegExp) return new RegExp(obj)
    if (typeof obj === 'symbol') {
      return Symbol(obj.description)
    }
    if (typeof obj === 'function') return cloneFunc(obj)

    let res = init(obj)
    if (obj instanceof Map) {
      obj.forEach((value, key) => {
        res.set(key, deepCopy(value, map))
      })
      return res
    }

    if (obj instanceof Set) {
      obj.forEach((value) => {
        res.add(deepCopy(value, map))
      })
      return res
    }

    if (map.get(obj)) return map.get(obj)
    map.set(obj, res)

    for (const [key, value] of Object.entries(obj)) {
      if (value && typeof value === 'object') {
        res[key] = deepCopy(value, map)
      } else {
        res[key] = value
      }
    }
    return res
  }
  return obj
}

const obj = {
    a: {
        b: {
            c: 1,
            d: void 0,
            e: Symbol.for('symbol'),
            f: function(){},
            g: NaN,
            [Symbol.for('symbol')]: 'symbol'
        }
    }
}
obj.a.b.h = obj
const newObj = deepCopy(obj)
console.log(newObj)

好了,现在克隆全部完结。

测试

如何进行测试呢? 首先需要 mock 数据,你可以简单的生成 1000 条数据,然后调用不同克隆方法执行它。但是 V8 有一个机制:当你添加属性到一个对象时,V8有一个缓存,所以其实是在给缓存做基准测试。为了确保数据永远不会碰到缓存,需要编写一个随机函数,每次生成不同的对象,然后在运行测试。以下是运行的结果: 这是我扩展测试的 代码

image.png

数值越小,效果越好!