JavaScript 浅拷贝:只复制“第一层”的艺术

📋 JavaScript 浅拷贝:只复制"第一层"的艺术

🤔 什么是浅拷贝?

定义 :

浅拷贝是指创建一个新对象,这个新对象拥有原对象属性值的精确拷贝。

  • 如果属性是基本类型 (String, Number, Boolean...),拷贝的是值。
  • 如果属性是引用类型 (Object, Array...),拷贝的是内存地址(指针)。

通俗比喻 :

想象你有一本相册(原对象)。

  • 浅拷贝 :你复印了相册的封面和目录。对于照片本身,你没有重新打印,而是把原相册里的照片拿出来,放进了新相册。
    • 结果:如果你在新相册里撕掉一张照片(修改引用类型的内部属性),原相册里的那张照片也坏了,因为它们其实是同一张物理照片。
    • 但是,如果你给新相册换了一个封面(修改第一层基本属性),原相册不受影响。

📂 目录

  1. [🖼️ 图解:浅拷贝 vs 深拷贝](#🖼️ 图解:浅拷贝 vs 深拷贝)
  2. [🛠️ 实现浅拷贝的 4 种主流方法](#🛠️ 实现浅拷贝的 4 种主流方法)
  3. [⚠️ 浅拷贝的陷阱:什么时候会出问题?](#⚠️ 浅拷贝的陷阱:什么时候会出问题?)
  4. [💡 总结与选型建议](#💡 总结与选型建议)

1. 🖼️ 图解:浅拷贝 vs 深拷贝

假设我们有如下数据结构:

javascript 复制代码
const original = {
  name: "Lingma", // 基本类型
  info: {
    // 引用类型
    age: 1,
  },
};

✅ 浅拷贝后内存结构

text 复制代码
Stack (栈)
+----------+             Heap (堆)
| original |-----------> +------------------+
+----------+             | name: "Lingma"   |
                         | info: [Addr A] --+--> +------------------+
+----------+             +------------------+     | age: 1           |
| copy     |-----------> +------------------+     +------------------+
+----------+             | name: "Lingma"   |
                         | info: [Addr A] --+----^ (指向同一个对象!)
                         +------------------+

结论 :original.info 和 copy.info 指向堆中的同一个对象。

❌ 深拷贝后内存结构(对比参考)

text 复制代码
Stack (栈)
+----------+             Heap (堆)
| original |-----------> +------------------+
+----------+             | name: "Lingma"   |
                         | info: [Addr A] --+--> +------------------+
+----------+             +------------------+     | age: 1           |
| copy     |-----------> +------------------+     +------------------+
+----------+             | name: "Lingma"   |
                         | info: [Addr B] --+--> +------------------+
                         +------------------+     | age: 1           |
                                                  +------------------+

结论 :copy 拥有完全独立的内存空间。


2. 🛠️ 实现浅拷贝的 4 种主流方法

方法一:Object.assign() (经典常用)

这是 ES6 之前最常用的浅拷贝方法。它将源对象的所有可枚举属性复制到目标对象。

javascript 复制代码
const original = { a: 1, b: { c: 2 } };
const copy = Object.assign({}, original);

// 测试基本类型
copy.a = 99;
console.log(original.a); // 1 ✅ 互不影响

// 测试引用类型
copy.b.c = 99;
console.log(original.b.c); // 99 😱 原数据被修改!

注意 :第一个参数 {} 是目标对象,必须传空对象,否则 original 自身会被修改。

方法二:展开运算符 ... (现代首选)

ES6 引入的语法糖,代码更简洁,可读性更高。

javascript 复制代码
const original = { a: 1, b: { c: 2 } };
const copy = { ...original };

// 效果同 Object.assign
copy.b.c = 99;
console.log(original.b.c); // 99 😱

对于数组的浅拷贝:

javascript 复制代码
const arr = [1, 2, [3, 4]];
const newArr = [...arr];
// 或者
const newArr2 = arr.slice();
// 或者
const newArr3 = Array.from(arr);

方法三:Array.prototype.slice() / concat() (数组专用)

在 ES6 之前,这是数组浅拷贝的标准做法。

javascript 复制代码
const arr = [1, 2, 3];
const copy1 = arr.slice();
const copy2 = [].concat(arr);

方法四:手写浅拷贝函数(面试/定制需求)

如果需要兼容旧环境或处理特殊逻辑,可以手写。

javascript 复制代码
function shallowClone(obj) {
  if (obj === null || typeof obj !== "object") {
    return obj;
  }

  // 判断是数组还是对象
  const cloneObj = Array.isArray(obj) ? [] : {};

  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      cloneObj[key] = obj[key]; // 直接赋值,引用类型拷贝地址
    }
  }

  return cloneObj;
}

3. ⚠️ 浅拷贝的陷阱:什么时候会出问题?

浅拷贝最大的风险在于嵌套对象。

❌ 事故现场

javascript 复制代码
const user = {
  name: "Lingma",
  settings: {
    theme: "dark",
    notifications: true,
  },
};

// 浅拷贝
const newUser = { ...user };

// 用户修改设置
newUser.settings.theme = "light";

console.log(user.settings.theme); // "light" 😱
// 糟糕!原用户的设置也被改了!

✅ 如何避免?

  1. 如果只有一层嵌套:使用浅拷贝完全没问题,性能最好。
  2. 如果有多层嵌套 :
    • 使用深拷贝 (structuredClone, JSON.parse/stringify, Lodash cloneDeep)。
    • 或者使用不可变数据库(如 Immer, Immutable.js),它们通过结构共享实现了高效的"看似可变,实则不可变"。

4. 💡 总结与选型建议

方法 适用场景 优点 缺点
{ ...obj } 对象浅拷贝 语法简洁,现代标准 仅浅拷贝,兼容性需 babel
Object.assign 对象浅拷贝/合并 兼容性好,可合并多个对象 语法稍啰嗦
[...arr] 数组浅拷贝 简洁,速度快 仅适用于数组/可迭代对象
arr.slice() 数组浅拷贝 经典写法,兼容 IE 语义不如展开运算符清晰

🚀 博主寄语 :

浅拷贝不是"低级"的拷贝,而是高性能 的拷贝。

在 React/Vue 的状态管理中,如果状态层级不深,浅拷贝配合不可变思维(Immutability)是最佳实践。

记住原则:

  1. 一层用浅,多层用深。
  2. 数组展开最香,对象合并 assign。
  3. 引用类型要小心,修改之前想清楚。

希望这篇文档能帮你彻底掌握浅拷贝!如果有疑问,欢迎在评论区留言。👇

喜欢这篇文章吗?记得点赞、收藏、转发哦! ❤️

相关推荐
吴可可1231 小时前
WScript Shell操作注册表详解
开发语言
纪念 2292 小时前
C++ 算法基础 (差分)
开发语言·c++·算法
johnjincaaaa4 小时前
猿人学第 18 题「jsvmp 洞察先机」逆向通关实录
javascript·后端
码农小河666 小时前
基于PHP的电商后台蜜罐系统
开发语言·php
starzy19906 小时前
深入理解Scala柯里化函数:函数链式变换
开发语言·scala
ShineWinsu6 小时前
对于Qt:QTreeWidget常见属性以及QTreeWidgetItem类的解析
开发语言·c++·qt·gui·客户端·多叉树·qtreewidget
落羽的落羽7 小时前
【C++】C++17的主要特性
linux·服务器·开发语言·c++·算法·机器学习
FungLeo7 小时前
成为全栈·Vue 管理后台篇·分类树与标签管理:层级数据如何落到管理界面
前端·javascript·vue.js·分类树·naive ui·标签管理·前端数据结构
总要学点什么7 小时前
第108篇:三维特效-图片轨迹线效果——把贴图滚成一串箭头,想用它的本色反而整条消失
开发语言·javascript·ecmascript·gis·webgl·材质·cesium
小厨勺子11 小时前
我用AIUI给Rokid眼镜做了个随身换算智能体
javascript