什么是拷贝?为什么需要它?

在 JS 中,当我们把一个对象赋值给另一个变量时,实际上只是复制了对象的引用(地址),而不是对象本身。这就像是两个人拿着同一把钥匙,指向同一个房间。

const person1 = { name: '小明', age: 25 }
const person2 = person1

person2.name = '小红'
console.log(person1.name) // 输出 '小红'

看到了吗?我们只改了 person2,但 person1 也变了,因为它们指向同一个对象。有时这不是我们想要的结果,这就需要拷贝出来一个新对象。

浅拷贝

浅拷贝好比是复印了对象的"第一层"。它会创建一个新对象,但只复制原对象第一层属性的值。如果属性是基本类型(数字、字符串等),会复制值;如果是引用类型(对象、数组),则只复制引用地址。

浅拷贝的常用方法:

// 方法一:Object.assign()
const original = { name: '小明', hobbies: ['跑步', '游泳'] }
const copy = Object.assign({}, original)

// 方法二:展开运算符
const copy2 = { ...original }

// 方法三:数组的浅拷贝
const array = [1, 2, { a: 3 }]
const arrayCopy = [...array]
// 或者
const arrayCopy2 = array.slice()

看看浅拷贝的效果:

original.name = '小红'
original.hobbies.push('篮球')

console.log(copy.name) // '小明',没变,因为是基本类型
console.log(copy.hobbies) // ['跑步', '游泳', '篮球'],变了!因为是引用类型

深拷贝

深拷贝则是彻底的复制,会递归地复制所有层级的属性,创建一个全新的对象,两者完全独立,互不影响。

深拷贝的实现方法:

// 方法一:JSON 转换(有局限性)
const original = { name: '小明', hobbies: ['跑步', '游泳'] }
const copy = JSON.parse(JSON.stringify(original))

// 方法二:手写递归函数
function deepClone(obj) {
  if (obj === null || typeof obj !== 'object') return obj
  
  const result = Array.isArray(obj) ? [] : {}
  
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      result[key] = deepClone(obj[key])
    }
  }
  
  return result
}

const copy2 = deepClone(original)

// 方法三:使用第三方库如 lodash
// const copy3 = _.cloneDeep(original)

测试一下效果:

original.name = '小红'
original.hobbies.push('篮球')

console.log(copy.name) // '小明',没变
console.log(copy.hobbies) // ['跑步', '游泳'],也没变!

两者的区别和选择

  • 浅拷贝:性能好,但只能处理第一层属性
  • 深拷贝:完全独立,但性能消耗大

选择哪种方式取决于你的需求:

  • 如果对象结构简单,只有一层,或者你只关心第一层属性的独立性,用浅拷贝就够了
  • 如果需要完全独立的对象副本,就需要深拷贝

注意事项

  1. JSON.parse(JSON.stringify()) 这种深拷贝方法有限制:
    • 不能处理函数、undefined、Symbol、循环引用
    • 会丢失原型链
    • 无法正确处理 Date、RegExp 等特殊对象
  2. 递归实现深拷贝要注意防止循环引用,避免栈溢出