JS 拷贝:浅拷贝 / 深拷贝原理 + 常用方法

前言

在JavaScript中,拷贝是我们日常开发中高频用到的操作,但很多小伙伴会被"浅拷贝"和"深拷贝"搞晕------为什么有时候修改原对象,新对象也跟着变?有时候却完全不受影响?

其实答案很简单:拷贝只针对引用类型(对象、数组等),基本类型(数字、字符串、布尔值)不存在"深浅"一说(赋值就是直接拷贝值)。今天我们就结合具体代码,把浅拷贝和深拷贝讲透。

先明确核心概念

无论是浅拷贝还是深拷贝,核心都是「基于原对象,创建一个新对象」,区别在于:拷贝的"深度"不同,是否会拷贝原对象内部的子引用类型。

重点牢记:引用类型的赋值,赋值的是「内存地址」,不是值本身;而拷贝是创建新对象,只是拷贝的"层数"有差异。

一、浅拷贝:只抄"表面",子对象还是"共用"

浅拷贝的特点:只拷贝原对象的第一层属性。如果原对象的属性值是引用类型(比如子对象、数组),那么拷贝的不是值,而是这个子对象的内存地址------也就是说,原对象和新对象的子引用类型,共用同一个"空间",修改其中一个,另一个会跟着变。

1. 数组浅拷贝:slice() 方法

javascript 复制代码
const arr = ['a', 'b', 'c', 'd', {age: 18}]
const arr2 = arr.slice(0) // 浅拷贝数组arr(slice不传参、传0效果一致,均拷贝整个数组)
arr[4].age = 20 // 修改原数组中的子对象属性
console.log(arr2); // 输出:['a','b','c','d',{age:20}]

🔍 解析:slice(0) 会创建一个新数组,拷贝原数组的第一层元素。但数组中第4个元素是对象(引用类型),拷贝的是它的地址,所以修改原数组中这个对象的age,新数组arr2中的对应对象也会同步变化。

2. 数组浅拷贝:扩展运算符 [...arr]

javascript 复制代码
const a = [1, 2, {age: 18}]
let c = [...a] // 用扩展运算符浅拷贝数组a
a[2].age = 19 // 修改原数组中的子对象属性
console.log(c); // 输出:[1,2,{age:19}]

🔍 解析:扩展运算符是最简洁的数组浅拷贝方式之一,原理和slice()一致,只拷贝第一层。子对象依然是共用地址,修改原对象子属性,新对象会受影响。

3. 数组浅拷贝:concat() 方法

javascript 复制代码
const a = [1, 2, 3, {age: 18}]
const d = [].concat(a) // 用concat()浅拷贝数组a
a[3].age = 20 // 修改原数组中的子对象属性
console.log(d); // 输出:[1,2,3,{age:20}]

🔍 解析:concat() 原本用于拼接数组,当只传入一个原数组时,会返回一个新数组,本质也是浅拷贝,子引用类型依然共用地址。

4. 数组浅拷贝:toReversed().reverse() (特殊小技巧)

javascript 复制代码
const arr = [1, 2, 3]
const arr2 = arr.toReversed().reverse() // 先反转再反转,得到原数组的浅拷贝
console.log(arr2); // 输出:[1,2,3]

🔍 解析:toReversed() 会返回一个反转后的新数组(不修改原数组),再调用reverse() 反转回来,相当于创建了一个和原数组完全一样的新数组,属于浅拷贝。

5. 对象浅拷贝:Object.assign() 方法

javascript 复制代码
const obj = {
  name: '跳跳虎',
  like: ['侦探'] // 子属性是数组(引用类型)
}
const newObj = Object.assign({}, obj) // 浅拷贝obj到新对象
obj.like[0] = '蹦蹦跳跳' // 修改原对象的子数组
console.log(newObj); // 输出:{name: '跳跳虎', like: ['蹦蹦跳跳']}

🔍 解析:Object.assign(target, source) 会将source对象的属性拷贝到target对象,返回target。这里target是空对象,相当于创建了一个新对象,拷贝原对象的第一层属性。子数组like是引用类型,拷贝的是地址,所以修改原对象的like,newObj的like也会变。

6. 手动实现浅拷贝

javascript 复制代码
const obj = {
  name: '跳跳虎',
  age: 18,
  like: {
    n: '侦探',
    m: '蹦蹦跳跳'
  }
}

// 自定义浅拷贝函数
function shallowCopy(obj) {
  let o = {} // 创建新对象
  // 遍历原对象,拷贝第一层属性(只拷贝自身可枚举属性)
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) { // 排除原型链上的属性
      o[key] = obj[key] // 引用类型拷贝地址,基本类型拷贝值
    }
  }
  return o
}

const newObj = shallowCopy(obj)
obj.like.m = '麦芽糖' // 修改原对象的子对象属性
console.log(newObj); // 输出:like.m 也变成了'麦芽糖'

🔍 解析:手动实现的浅拷贝逻辑很简单------创建新对象,遍历原对象的自身属性,将属性值赋值给新对象。和内置方法一样,子引用类型拷贝的是地址,会受原对象影响。

二、深拷贝:层层拷贝,完全"独立"

深拷贝的特点:层层拷贝,直到所有层级的属性都是基本类型。新对象和原对象完全独立,修改原对象的任何属性(包括子对象、子数组),新对象都不会有任何变化------相当于"复制粘贴"了一整个对象,连里面的"小零件"都复制了一份。

1. 深拷贝:structuredClone() 方法

javascript 复制代码
const obj = {
  name: '跳跳虎',
  age: 18,
  like: {
    n: '侦探',
    m: '蹦蹦跳跳'
  },
  a: 123n, // BigInt类型
  date: new Date(), // Date类型
  map: new Map([['key1', 'value1']]), // Map类型
  say() {
    console.log('hello');
  }
}

const newObj = structuredClone(obj) // 深拷贝obj
obj.like.m = '麦芽糖' // 修改原对象的子对象属性
obj.date.setFullYear(2025) // 修改原对象的Date属性
console.log(newObj); 
// 输出:like.m 依然是'蹦蹦跳跳',date依然是原日期(不受影响)
// 注意:say()函数被忽略,map被正常拷贝

🔍 解析:structuredClone() 是ES2022新增的深拷贝方法,用法简单,能处理大部分引用类型(对象、数组、Map、Set、Date、RegExp、ArrayBuffer等),支持BigInt类型,且能正确拷贝循环引用(比如obj.self = obj,不会报错)。

注意:structuredClone() 有以下局限:

  • 无法拷贝函数(普通函数、箭头函数、类方法均会被忽略);
  • 无法拷贝原型链上的属性(只拷贝对象自身属性);
  • 无法拷贝Error、Function、GeneratorFunction、AsyncFunction等类型;
  • 无法拷贝Symbol类型(会被忽略);
  • 无法拷贝DOM节点(会报错)。

2. 深拷贝:JSON.parse(JSON.stringify(obj))

javascript 复制代码
const obj = {
  name: '跳跳虎',
  age: 18,
  like: {
    n: '侦探',
    m: '蹦蹦跳跳'
  },
  say() {
    console.log('hello');
  },
  a: undefined, // undefined类型
  b: null,
  c: NaN, // NaN类型
  d: Infinity // 无穷大类型
}

const oo = JSON.parse(JSON.stringify(obj)) // 深拷贝
obj.like.m = '麦芽糖' // 修改原对象
console.log(oo); 
// 输出:{name: '跳跳虎', age:18, like:{n:'侦探',m:'蹦蹦跳跳'}, b:null, c:null, d:null}

🔍 解析:这是最常用的"民间深拷贝方法",原理是:先将对象转为JSON字符串(JSON.stringify),再将字符串转回对象(JSON.parse),相当于重新创建了一个完全独立的对象。

注意:这个方法有明显局限,无法处理以下类型,会自动忽略或转换:

  • 函数(say()方法被忽略)

  • BigInt(会报错,无法转换)

  • undefined(被忽略)

  • NaN、Infinity(会转为null)

3. 手动实现深拷贝

javascript 复制代码
const obj = {
  name: '跳跳虎',
  age: 18,
  like: {
    n: '侦探',
    m: '蹦蹦跳跳'
  }
}

// 自定义递归深拷贝函数
function deepClone(obj) {
  let o = {} // 创建新对象
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) { // 只拷贝自身属性
      // 判断当前属性值是否是引用类型(且不是null,因为null的typeof也是object)
      if (typeof(obj[key]) == 'object' && obj[key] !== null) {
        // 递归调用深拷贝,对子对象也进行深拷贝
        const childObj = deepClone(obj[key])
        o[key] = childObj
      } else {
        // 基本类型,直接拷贝值
        o[key] = obj[key]
      }
    }
  }
  return o
}

const newObj = deepClone(obj)
obj.like.m = '麦芽糖' // 修改原对象
console.log(newObj); // 输出:like.m 依然是'蹦蹦跳跳'(完全独立)

🔍 解析:手动深拷贝的核心是「递归」------遍历对象时,如果遇到引用类型(且不是null),就递归调用自身,对这个子对象也进行深拷贝;如果是基本类型,就直接赋值。这样就能实现"层层拷贝",新对象和原对象完全独立。

补充:这个基础版递归深拷贝,还可以优化(比如处理数组、Map、Set等),但日常开发中,处理普通对象和数组已经足够啦~

三、补充知识点:for...in 遍历与hasOwnProperty()

在上面的拷贝函数中,我们都用到了 for (let key in obj)obj.hasOwnProperty(key),很多小伙伴会疑惑这两个的作用,这里简单补充:

javascript 复制代码
// 给Object原型添加一个属性(模拟原型链上的属性)
Object.prototype.d = 4
const obj = {
  a: 1,
  b: 2,
  c: 3
}

// for...in 会遍历对象自身的属性 + 原型链上的可枚举属性
for (let key in obj) {
  // hasOwnProperty(key):判断key是否是对象自身的属性(排除原型链上的属性)
  if (obj.hasOwnProperty(key)) {
    console.log(key); // 输出:a, b, c(不会输出d)
  }
}

🔍 解析:拷贝时,我们只需要拷贝对象「自身的属性」,不需要拷贝原型链上的属性(比如上面的d属性),所以用hasOwnProperty() 来过滤,避免拷贝多余的属性。

四、浅拷贝vs深拷贝 总结表

类型 核心特点 常用方法 注意事项
浅拷贝 只拷贝第一层,子引用类型共用地址 slice()、[...arr]、concat()、Object.assign() 修改原对象子引用类型,新对象会受影响
深拷贝 层层拷贝,新对象与原对象完全独立 structuredClone()、JSON.parse(JSON.stringify()) 各方法有局限(如无法拷贝函数、BigInt等)

最后小总结

  1. 基本类型不用纠结深浅拷贝,赋值就是拷贝值;

  2. 浅拷贝适合简单场景(原对象只有第一层基本类型),高效简洁;

  3. 深拷贝适合复杂场景(原对象有多层引用类型),保证数据独立,避免意外修改;

  4. 选择拷贝方法时,根据需求选:

  • 需拷贝Date、Map、循环引用 → 用structuredClone();
  • 简单对象/数组,无特殊类型 → 用JSON.parse(JSON.stringify())(注意局限);
  • 有特殊类型(如函数、Symbol)、需自定义拷贝规则 → 用完善版手动深拷贝;
  • 简单数组/对象,无需深层拷贝 → 用扩展运算符、Object.assign()等浅拷贝方法

结合上面的代码实例,能轻松区分浅拷贝和深拷贝,下次遇到拷贝问题,再也不用慌啦!

相关推荐
敲代码的约德尔人2 小时前
Vue 3 响应式系统完全指南:我在 4 个项目中踩坑后总结的血泪经验
前端·vue.js
始持2 小时前
第十四讲 网络请求与数据解析
前端·flutter
Roselind_Yi2 小时前
技术拆解:《从音频到动效:我是如何用 Web Audio API 拆解音乐的?》
前端·javascript·人工智能·音视频·语音识别·实时音视频·audiolm
和科比合砍81分2 小时前
pnpm:public-hoist-pattern[]配置
前端
我叫黑大帅2 小时前
Js常用数组处理
前端·javascript·面试
敲代码的约德尔人2 小时前
React useEffect 完全指南:我在 3 个项目中踩坑后总结的血泪经验
前端·react.js
小凡同志2 小时前
React 组件设计模式:从 HOC 到 Render Props 再到 Hooks
前端·react.js
毛骗导演2 小时前
OpenClaw Auth Profile 与多 Key 冷却隔离机制深度解析:一个 API Key 是如何被选择、追踪并轮换的
前端·架构
用户9751470751362 小时前
如何在 Vite 中配置 CSS 模块,以避免全局样式被模块化隔离覆盖?
前端