JS深浅拷贝全解析|常用方法+手写实现+避坑指南(附完整代码)

0 阅读4分钟

一、拷贝

  • 浅拷贝和深拷贝都复制了值和地址,都是为了解决引用类型赋值后互相影响的问题。
  • 但是浅拷贝只进行一层复制,深层次的引用类型还是共享内存地址,原对象和拷贝对象还是会互相影响。
  • 深拷贝就是无限层级拷贝,深拷贝后的原对象不会和拷贝对象互相影响。

二、浅拷贝

可实现浅拷贝的方式如下:

1. Object.assign

const obj = {
  name: 'lin'
}
const newObj = Object.assign({}, obj)
obj.name = 'xxx' // 改变原来的对象
console.log(newObj) // { name: 'lin' } 新对象不变
console.log(obj == newObj) // false 两者指向不同地址

2. 数组的slice和concat方法

const newArr = arr.slice(0)

arr[2] = 'rich' // 改变原来的数组

console.log(newArr) // ['lin', 'is', 'handsome']

console.log(arr == newArr) // false 两者指向不同地址
const arr = ['lin', 'is', 'handsome']
const newArr = [].concat(arr)

arr[2] = 'rich' // 改变原来的数组

console.log(newArr) // ['lin', 'is', 'handsome'] // 新数组不变

console.log(arr == newArr) // false 两者指向不同地址

3. 数组的静态方法 Array.from

const arr = ['lin', 'is', 'handsome']
const newArr = Array.from(arr)

arr[2] = 'rich' // 改变原来的数组

console.log(newArr) // ['lin', 'is', 'handsome']

console.log(arr == newArr) // false 两者指向不同地址

4.扩展运算符

const arr = ['lin', 'is', 'handsome']
const newArr = [...arr]

arr[2] = 'rich' // 改变原来的数组

console.log(newArr) // ['lin', 'is', 'handsome'] // 新数组不变

console.log(arr == newArr) // false 两者指向不同地址
const obj = {
  name: 'lin'
}

const newObj = { ...obj }

obj.name = 'xxx' // 改变原来的对象

console.log(newObj) // { name: 'lin' } // 新对象不变

console.log(obj == newObj) // false 两者指向不同地址

5. 循环遍历赋值

function clone (obj) {
  const cloneObj = {} // 创建一个新的对象
  for (const key in obj) { // 遍历需克隆的对象
    cloneObj[key] = obj[key] // 将需要克隆对象的属性依次添加到新对象上
  }
  return cloneObj
}

三、深拷贝

1. 序列化

JSON.parse(JSON.stringify(obj))

const obj = {
  person: {
    name: 'lin'
  }
}

const newObj = JSON.parse(JSON.stringify(obj))
obj.person.name = 'xxx' // 改变原来的深层对象

console.log(newObj) // { person: { name: 'lin' } } 新的深层对象不变

使用序列化的方式来实现深度克隆有些许弊端;

  • 会忽略undefinedsymbol函数
  • NaN Infinity -Infinity会被序列化为null
  • Map序列化返回是空对象{}; 个人认为Map的结构类似键值对,然后value是个函数,因函数的缘故无法序列化

2. 递归实现

要求:

  • 支持对象、数组、日期、正则的拷贝。
  • 处理原始类型(原始类型直接返回,只有引用类型才有深拷贝这个概念)。
  • 处理 Symbol 作为键名的情况。
  • 处理函数(函数直接返回,拷贝函数没有意义,两个对象使用内存中同一个地址的函数,问题不大)。
  • 处理 DOM 元素(DOM 元素直接返回,拷贝 DOM 元素没有意义,都是指向页面中同一个)。
  • 额外开辟一个储存空间 WeakMap,解决循环引用递归爆栈问题(引入 WeakMap 的另一个意义,配合垃圾回收机制,防止内存泄漏)。

Reflect.ownKeys() 方法返回一个由目标对象(自身)的属性键组成的数组(包括Symbol);它的返回值等同于Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))

function getType(target) {
  return Object.prototype.toString.call(target).slice(8, -1)
}
function deepClone(target, hash = new WeakMap()) {
  // 处理 原始值 null、undefined、number、string、symbol、bigInt、boolean
  if (typeof target !== 'object' || target === null) {
    return target
  }
  // 处理 array
  if (Array.isArray(target)) {
    return target.map((e) => deepClone(e))
  }
  // 处理 function
  if (getType(target) === 'Function') {
    return eval(`(${target.toString()})`).bind(this) // function 声明需要用"("、")"包裹
  }
  // 拷贝日期 
  if(getType(target) === 'Date') {
    return new Date(target.valueOf()) 
 }
  // 拷贝正则
  if(getType(target) === 'RegExp') {
    return new RegExp(target)
 }
  // 处理 map
  if (getType(target) === 'Map') {
    let map = new Map()
    target.forEach((v, k) => {
      map.set(k, deepClone(v))
    })
    return map
  }
  // 处理 set
  if (getType(target) === 'Set') {
    let set = new Set()
    for (let val of target.values()) {
      set.add(deepClone(val))
    }
    return set
  }
    
  if (hash.get(target) return hash.get(target) // 当需要拷贝当前对象时,先去存储空间中找,如果有的话直接返回
  const cloneTarget = new target.constructor() // 通过target的构造函数创建一个新的与之一样类型的对象,这样写的就不需要判断类型了
  hash.set(target, cloneTarget) // 如果存储空间中没有就存进存储空间 hash 里

  // 处理 object
  if (getType(target) === 'Object') {
	Reflect.ownKeys(target).forEach(key => {
    	cloneTarget[key] = deepClone(target[key], hash) // 递归拷贝每一层
  	})
    return cloneTarget
  }
  return target
}

3. structuredClone

结构化克隆算法是由HTML5规范定义的用于复制复杂JavaScript对象的算法;H5定义的全局深度克隆方法;

const obj = {
  person: {
    name: 'lin'
  }
}

const newObj = structuredClone(obj) // 
obj.person.name = 'xxx' // 改变原来的对象

console.log('原来的对象', obj)
console.log('新的对象', newObj)

console.log('更深层的对象指向不同的地址', obj.person == newObj.person)

缺点:

  • Error 以及 Function 对象是不能被结构化克隆算法复制的;如果你尝试这样子去做,这会导致抛出 DATA_CLONE_ERR 的异常。

  • 企图去克隆 DOM 节点同样会抛出 DATA_CLONE_ERR 异常。

  • 对象的某些特定参数也不会被保留

    • RegExp对象的 lastIndex 字段不会被保留
    • 属性描述符,setters 以及 getters(以及其他类似元数据的功能)同样不会被复制。例如,如果一个对象用属性描述符标记为 read-only,它将会被复制为 read-write,因为这是默认的情况下。
    • 原形链上的属性也不会被追踪以及复制。
  • Symbol除外可复制

最后说一局,推荐使用lodash,lodash的实现是很全面的

感谢您抽出宝贵的时间观看本文;本文是 JavaScript 系列的第 5 篇,后续会持续更新!欢迎关注~