JS高级 - 拷贝

145 阅读5分钟

对象赋值关系

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

引用赋值

const user1 = {
  name: 'Klaus',
  friend: {
    name: 'Alex'
  }
}

const user2 = user1

user1.name = 'Steven'
console.log(user2.name) // => Steven

浅拷贝

浅拷贝实现主要有两种方式:

  1. 展开运算符
  2. Object.assign方法
const user1 = {
  name: 'Klaus',
  friend: {
    name: 'Alex'
  }
}

const user2 = { ...user1 }

user1.name = 'Steven'
console.log(user2.name) // => Klaus

user1.friend.name = 'Jhon'
console.log(user2.friend.name) // => Jhon
const user1 = {
  name: 'Klaus',
  friend: {
    name: 'Alex'
  }
}

// Object.assign将参数2 和 参数1 进行合并
// 如果属性在参数1和参数2中 同时存在,则参数2的属性值会覆盖参数1中的属性值
// 如果属性在参数1中存在,参数2中不存在,那么依旧会存在于返回的新对象中
// 如果属性在参数2中存在,参数1中不存在,那么会被添加到新返回的对象中

// 所以在Object.assign方法中,如果参数1的值为空对象的时候
// 返回的新对象 其实 就是参数2的 浅拷贝对象
const user2 = Object.assign({}, user1)
console.log(user2)

user1.name = 'Steven'
console.log(user2.name) // => Klaus

user1.friend.name = 'Jhon'
console.log(user2.friend.name) // => Jhon

深拷贝

const user1 = {
  name: 'Klaus',
  friend: {
    name: 'Alex'
  }
}

// 可以使用JSON.parse和JSON.stringify方法实现对象的深拷贝
const user2 = JSON.parse(JSON.stringify(user1))

user1.name = 'Peter'
user1.friend.nam = 'Alice'

console.log(user2) // => { name: 'Klaus', friend: { name: 'Alex' } }

但是默认情况下,JS并没有实现对象深拷贝方法,使用JSON来实现深拷贝只是一种尽可能的模拟实现

如果对象中存在方法,Symbol值,或 undefined,使用JSON来进行深拷贝的时候,就会显得无能为力

因为JSON不支持undefined,Symbol值 和 方法

const user1 = {
  name: 'Klaus',
  friend: {
    name: 'Alex'
  },
  running() {
    console.log('running')
  },
  [Symbol()]: 'symbol',
  address: undefined
}

const user2 = JSON.parse(JSON.stringify(user1))

// JSON不支持方法,undefined 和 symbol类型值
console.log(user2) // => { name: 'Klaus', friend: { name: 'Alex' } }

此外, 如果对象中存在循环引用,例如对象自身有一个属性的属性值指向自己,那么JSON就无法将其转换为对应的字符串

自然也就没有办法使用JSON来实现对象的深拷贝了

const user1 = {
  name: 'Klaus',
  friend: {
    name: 'Alex'
  }
}

user1.user = user1

const user2 = JSON.stringify(user1) // error

console.log(user2)

所以,如果我们需要实现真正意义上的深拷贝,我们需要自己手动进行编写,或使用lodash/underscore等第三方库

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>深拷贝</title>
</head>
<body>
  <input type="text" id="foo">

  <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
  <script src="./index.js">
    const user1 = {
      name: 'Klaus',
      friend: {
        name: 'Alex'
      },
      running() {
        console.log('running')
      },
      [Symbol()]: 'symbol',
      address: undefined
    }

    user1.user = user1

    const user2 = _.cloneDeep(user1)

    console.log(user2)
  </script>
</body>
</html>

手动实现深拷贝

基本实现

const user1 = {
  name: 'Klaus',
  friend: {
    name: 'Alex'
  }
}

// 判断参数是基本数据类型 还是 复杂数据类型
function isObject(param) {
  return (param !== null) && ['function', 'object'].includes(typeof param)
}

function deepClone(origin) {
  // 如果是基本数据类型,那么就直接返回基本数据类型
  if (!isObject(origin)) return origin

  // 判断对象是否是数组,并进行对应的初始化操作
  const obj = Array.isArray(origin) ? [] : {}

  // 使用递归依次进行拷贝
  for (const key in origin) {
    obj[key] = deepClone(origin[key])
  }

  return obj
}

const user2 = deepClone(user1)
console.log(user2)

其它数据类型处理

在实际深拷贝的过程中,可能会遇到set, map,symbol等多种其它数据类型值,此时就需要对于他们进行特殊处理

const user1 = {
  name: 'Klaus',
  friend: {
    name: 'Alex'
  },
  running() {
    console.log('running')
  },
  set: new Set([1, 2, 3]),
  map: new Map([['key1', 'value1'], ['key2', 'value2']]),
  symbol: Symbol('symbol属性值'),
  [Symbol('s1')]: 'key值为symbol的属性值'
}

function isObject(property) {
  return (property !== null) && ['function', 'object'].includes(typeof property)
}

function deepClone(origin) {
  // 如果属性值为Symbol的时候,单独新建一个Symbol值
  if (typeof origin === 'symbol') {
    // 如果origin.description不存在,origin.description获取的值即为undefined
    // 以undefined作为Symbol的描述符去创建Symbol的时候,描述符会自动忽略,最终输出值为 Symbol()
    return Symbol(origin.description)
  }

  // 基本数据类型 直接返回
  if (!isObject(origin)) return origin

  // 函数类型的主要功能是用于执行,对于函数而言没有必要因为深拷贝而在内存中创建一个新的函数
  // 所以如果属性值是函数,直接返回对应的函数对象即可
  if (typeof origin === 'function') {
    return origin
  }

  // 如果是Set类型 单独处理
  if (origin instanceof Set) {
    const set = new Set()

    // set是可迭代对象,只能通过for-of来获取其中的值
    // set是不可以使用for-in遍历的
    for (const value of origin) {
      set.add(value)
    }

    return set
  }

  // 如果是Map类型 单独处理
  if (origin instanceof Map) {
    const map = new Map()

    for(const [key, value] of origin) {
      map.set(key, value)
    }

    return map
  }

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

  // 默认情况下for-in只能迭代自身和自身原型链上可迭代的非Symbol属性
  for (const key in origin) {
    obj[key] = deepClone(origin[key])
  }

  // 对Symbol属性值单独迭代
  for (const sym of Object.getOwnPropertySymbols(origin)) {
    obj[Symbol(sym.description)] = deepClone(origin[sym])
  }

  return obj
}

const user2 = deepClone(user1)
console.log(user2)

循环引用

上述的示例代码是没有解决循环引用问题的,如果在上述示例代码中存在循环引用,那么就会出现无限递归的情况

所以我们需要对上述代码进行如下改进:

const user1 = {
  name: 'Klaus',
  friend: {
    name: 'Alex'
  }
}

// 产生循环引用
user1.self = user1

function isObject(property) {
  return (property !== null) && ['function', 'object'].includes(typeof property)
}

// 在deepClone的时候传入一个WeakMap实例
// 调用者调用deepClone方法的时候, 不需要传递第二个参数, 使用默认生成的Weakmap实例即可

// 在deepClone内部调用的时候,手动传入之前初始化生成的那个WeakMap实例,以确保deepClone方法
// 内部使用的WeakMap实例都是同一个
function deepClone(origin, map = new WeakMap()) {
  if (typeof origin === 'symbol') {
    return Symbol(origin.description)
  }

  if (!isObject(origin)) return origin

  if (typeof origin === 'function') {
    return origin
  }

  if (origin instanceof Set) {
    const set = new Set()

    for (const value of origin) {
      set.add(value)
    }

    return set
  }

  if (origin instanceof Map) {
    const map = new Map()

    for(const [key, value] of origin) {
      map.set(key, value)
    }

    return map
  }

  // 在创建新对象的时候,先去map上查找
  // 如果存在对应的对象就直接返回之前所创建的那个对象
  // 而不用自己再新建一个新的对象
  if (map.has(origin)) {
    return map.get(origin)
  }

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

  // 每创建一个新对象,就在map上挂载一下
  map.set(origin, obj)

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

  for (const sym of Object.getOwnPropertySymbols(origin)) {
    obj[Symbol(sym.description)] = deepClone(origin[sym], map)
  }

  return obj
}

const user2 = deepClone(user1)
console.log(user2)
console.log(user2.self === user2) // => true