JS Advance --- 深拷贝

111 阅读3分钟

这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战

对象相互赋值的存在三种情况:

  • 引入的赋值:指向同一个对象,相互之间会影响
  • 对象的浅拷贝:只是浅层的拷贝,内部引入对象时,依然会相互影响
  • 对象的深拷贝:两个对象不再有任何关系,不会相互影

JSON.parse

const obj = {
  name: 'Klaus',
  firend: {
    name: 'Alex'
  }
}

const info = JSON.parse(JSON.stringify(obj))

console.log(info === obj) // => false
console.log(info.firend === obj.firend) // => false

我们可以使用JOSN的序列化和反序列化操作进行对象的深拷贝

但是使用JSON.parse进行深拷贝存在着一些限制

  1. 对于函数、Symbol等是无法处理的, 因为JSON并不支持这种数据结构
  2. 并且如果存在对象的循环引用,也会报错的
    • 例如 存在obj.inner = obj ,此时JSON.parse会直接报错

第三方库

这里使用loadsh为例

const obj = [{ 'a': 1 }, { 'b': 2 }]
const deep = _.cloneDeep(obj)

obj[1].b = 3
console.log(deep)

自定义实现深拷贝

基本实现

function isObj(value) {
  const type = typeof value

  // typeof null = object
  if (value !== null && (type === 'object' || type === 'function')) {
    return true
  }
}

function deepClone(obj) {
  if (!isObj(obj)) {
    return obj
  }

  const newObj = {}

  for (let key in obj) {
    newObj[key] = deepClone(obj[key])
  }

  return newObj
}


const user = {
  name: 'Klaus',
  friend: {
    name: 'Alex',
    age: 23
  }
}

const newUser = deepClone(user)
user.friend.name = 'Steven'
console.log(newUser)

一些其它类型值的处理

function isObj(value) {
  const type = typeof value

  if (value !== null && (type === 'object' || type === 'function')) {
    return true
  }
}

function deepClone(obj) {
  // 函数体本身就是可以复用的
  if (typeof obj === 'function') {
    return obj
  }

  // 如果value是Symbol, 创建一个新的Symbol值后返回
  if (typeof obj === 'symbol') {
    return Symbol(obj.description)
  }
  
  if (!isObj(obj)) {
    return obj
  }

  if (obj instanceof Set) {
    // const set = new Set([1, 2, 3])
    // [...set] ==> [1, 2, 3]

    // 对set的浅拷贝
    // return new Set([...obj])

    // 深拷贝
    return new Set([...deepClone([...obj])])
  }

  if (obj instanceof Map) {
    // const map = new Map([['key', 'value'], ['k', 'v']])
    // [...map] ==> [['key', 'value'], ['k', 'v']]

    // 对map的浅拷贝
    // return new Map([...obj])

    // 深拷贝
    return new Map([...deepClone([...obj])])
  }

  // 避免将[1, 2, 3] 复制成 { '0': 1, '1': 1, '2': 2 }
  const newObj = Array.isArray(obj) ? [] : {}

  for (const key in obj) {
    newObj[key] = deepClone(obj[key])
  }

  // for..in... 无法获取到key为Symbol的属性
  const symbolKeys = Object.getOwnPropertySymbols(obj)

  for (const sKey of symbolKeys) {
    // symbol类型的值 主要是为了避免在一个对象中 多个key出现同名的情况
    // 所以在不同的对象中,没有必要特地为此创建不同的symbol值作为key
    newObj[sKey] = deepClone(obj[sKey])
  }

  return newObj
}


const user = {
  name: 'Klaus',
  arr: [1, 2, 3],

  friend: {
    name: 'Alex',
    age: 23
  },

  foo() {
    console.log('foo')
  },

  [Symbol('s1')]: 's1',
  s2: Symbol('s2'),

  set: new Set([1, 2, 3]),
  map: new Map([['key', 'value'], ['k', { value: 'v' }]])
}

const newUser = deepClone(user)
console.log(newUser)

循环引用

function isObj(value) {
  const type = typeof value

  if (value !== null && (type === 'object' || type === 'function')) {
    return true
  }
}


// 第一次使用的时候,不传入map, 那么就会创建一个新的map
// 以后使用的时候,在参数调用的时候传入map,那么就会使用传入的map,并不会再次新建一个map
function deepClone(obj, map = new WeakMap()) {
  if (typeof obj === 'function') {
    return obj
  }

  if (typeof obj === 'symbol') {
    return Symbol(obj.description)
  }

  if (!isObj(obj)) {
    return obj
  }

  if (obj instanceof Set) {
    return new Set([...deepClone([...obj], map)])
  }

  if (obj instanceof Map) {
    return new Map([...deepClone([...obj], map)])
  }

  // 如果引用相同,就直接返回直接引进存储的新拷贝对象的引用
  if (map.has(obj)) {
    return map.get(obj)
  }

  const newObj = Array.isArray(obj) ? [] : {}

  // 将拷贝新生成的对象放入map中
  map.set(obj, newObj)

  for (const key in obj) {
    newObj[key] = deepClone(obj[key], map)
  }

  const symbolKeys = Object.getOwnPropertySymbols(obj)

  for (const sKey of symbolKeys) {
    newObj[sKey] = deepClone(obj[sKey], map)
  }

  return newObj
}


const user = {
  name: 'Klaus',
  arr: [1, 2, 3],

  friend: {
    name: 'Alex',
    age: 23
  },

  foo() {
    console.log('foo')
  },

  [Symbol('s1')]: 's1',
  s2: Symbol('s2'),

  set: new Set([1, 2, 3]),
  map: new Map([['key', 'value'], ['k', { value: 'v' }]])
}

// 产生循环引用
// userInfo和user的引用地址是一致的
user.userInfo = user

const newUser = deepClone(user)
console.log(newUser)