一文搞懂 JavaScript 中的 copy:值拷贝、浅拷贝与深拷贝

79 阅读3分钟

一文搞懂 JavaScript 中的 copy:值拷贝、浅拷贝与深拷贝

刚学 JS 的时候,我经常遇到一个问题:明明拷贝了一个对象,结果改了新对象,原对象也跟着变了?
后来我才发现,这背后其实是 JavaScript 中“拷贝机制”没理解清楚
这篇文章是我学习 JS copy(拷贝)过程中的一次系统整理,希望能帮到和我一样的 JS 新手。

一、为什么 JS 的 copy 这么容易出问题?

先看一段代码:

const obj1 = { name: 'Tom', like: { sport: 'football' } }
const obj2 = obj1

obj2.like.sport = 'basketball'
console.log(obj1.like.sport) // basketball

很多新手都会疑惑:我只是改了 obj2,为什么 obj1 也变了?
原因只有一个:obj1 和 obj2 指向的是同一个对象引用

要理解 copy,必须先理解 JS 中的数据类型。

二、值类型 vs 引用类型

2.1 值类型(基本数据类型)
  • Number
  • String
  • Boolean
  • undefined
  • null
  • Symbol
  • BigInt

特点

  • 拷贝的是「值本身」
  • 修改互不影响
let a = 10
let b = a
b = 20
console.log(a) // 10
2.2 引用类型
  • Object
  • Array
  • Function
  • Date、Map、Set 等

特点

  • 变量中存的是“地址”
  • 多个变量可能指向同一块内存
let obj1 = { a: 1 }
let obj2 = obj1
obj2.a = 2
console.log(obj1.a) // 2

所以,拷贝对象时,如果只是拷贝引用,就会产生“联动修改”的问题

三、浅拷贝

什么是浅拷贝?

只拷贝对象的第一层
如果属性值还是对象,那么拷贝的仍然是引用。

常见的浅拷贝方式
数组
arr.slice()
[...arr]
arr.concat()
对象
Object.assign({}, obj)
{ ...obj }
手写一个浅拷贝
function shallowCopy(obj) {
  let result = {}
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      result[key] = obj[key]
    }
  }
  return result
}

测试一下:

const obj = {
  name: '玉米',
  like: {
    sport: 'football'
  }
}

const newObj = shallowCopy(obj)
newObj.like.sport = 'basketball'

console.log(obj.like.sport) // basketball ❌

问题依然存在:内部对象还是共享引用

四、深拷贝

什么是深拷贝?

递归地拷贝所有层级
新对象和原对象完全独立,互不影响。

五、常见的深拷贝方案对比

5.1 structuredClone(推荐)
const newObj = structuredClone(obj)

优点

  • 原生 API
  • 支持 Object、Array、Map、Set、Date 等
  • 能处理循环引用

缺点

  • 不支持函数
  • 旧浏览器不支持
5.2 JSON.parse(JSON.stringify())
const newObj = JSON.parse(JSON.stringify(obj))

优点

  • 写法简单
  • 兼容性好

缺点(非常重要)

  • 丢失 function
  • 丢失 undefined、Symbol、BigInt
  • NaN / Infinity 会被处理成 null
  • 无法处理循环引用

只能用于结构简单、数据纯净的对象

5.3 手写递归深拷贝
function deepClone(obj, map = new WeakMap()) {
  if (obj === null) return null
  if (typeof obj !== 'object') return obj

  if (map.has(obj)) return map.get(obj)

  const result = Array.isArray(obj) ? [] : {}
  map.set(obj, result)

  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      result[key] = deepClone(obj[key], map)
    }
  }
  return result
}

支持循环引用
未处理 Map / Set / Date / RegExp / Symbol 等
适合学习原理,不建议直接用于生产

六、深拷贝中最容易踩的坑

  • 函数无法被 JSON 或 structuredClone 拷贝
  • 循环引用会让 JSON.stringify 报错
  • Symbol / BigInt / undefined 会丢失
  • 原型链、不可枚举属性不会被普通拷贝方式处理

所以: 不要盲目深拷贝,先想清楚你到底需不需要


最后

理解 copy,不只是为了面试,更是为了写出不踩坑的代码。
这篇文章是我作为 JS 新手的学习总结,如果有理解不准确的地方,欢迎在评论区指出。如果你也被 copy 问题坑过,欢迎留言交流~