你需要知道javascript小知识:JavaScript 中的引用拷贝、浅拷贝与深拷贝详解

55 阅读4分钟

——附内存机制解析与大厂面试题


📌 前言

在 JavaScript 开发中,我们经常需要复制对象或数组。然而,直接赋值(=)并不会创建一个全新的副本,而是产生“引用共享”的现象。这常常导致意料之外的数据污染问题。理解 引用拷贝、浅拷贝、深拷贝 的区别,以及它们背后的 内存存储机制(栈 vs 堆) ,是掌握 JavaScript 核心原理的关键一步,也是大厂面试中的高频考点。

本文将结合你提供的代码示例,系统讲解三者的差异,并深入剖析其底层内存模型。


一、内存基础:栈(Stack)与堆(Heap)

JavaScript 引擎(如 V8)将内存分为两类:

内存区域存储内容特点
栈(Stack)基本类型(numberstringbooleannullundefinedsymbolbigint- 连续内存 - 分配/释放快 - 大小固定 - 按值访问
堆(Heap)引用类型(ObjectArrayFunction 等)- 动态分配 - 不连续 - 需要垃圾回收 - 通过引用地址访问

关键点:变量本身(如 users)存于栈中,但它的值是一个指向堆内存中对象的指针(地址)

js
编辑
let a = 1;          // a 存在栈中,值为 1(基本类型)
const users = [...]; // users 存在栈中,值是一个地址,指向堆中的数组对象

二、三种“拷贝”方式详解

1️⃣ 引用拷贝(Reference Copy)

js
编辑
const data = users; // 引用拷贝
data[0].hobbies = ["篮球", "看烟花"];
console.log(users[0]); // { id: "1", name: "饶世豪", hometown: "新建", hobbies: ["篮球", "看烟花"] }
  • 本质:只是复制了栈中的引用地址,两个变量指向同一个堆内存对象
  • 后果:修改任一变量,都会影响另一个。
  • 适用场景:不需要独立副本时(如只读操作)。

💡 这不是真正的“拷贝”,而是“别名”。


2️⃣ 浅拷贝(Shallow Copy)

js
编辑
const data2 = [...users]; // 或 Object.assign([], users)
  • 行为

    • 创建一个新的数组(堆中新开辟空间)
    • 但数组中的每个元素(对象)仍然是原对象的引用
  • 结果

    • 修改 data2 的顶层结构(如 data2.push(...))不会影响 users
    • 但修改嵌套对象属性(如 data2[0].name = "张三")会同时影响 users

✅ 示例验证:

js
编辑
data2[0].name = "张三";
console.log(users[0].name); // "张三" ← 被污染!
  • 常用方法

    • 数组:[...arr]arr.slice()Array.from(arr)
    • 对象:{...obj}Object.assign({}, obj)

3️⃣ 深拷贝(Deep Copy)

js
编辑
const data1 = JSON.parse(JSON.stringify(users));
  • 行为:递归地复制所有层级的对象,完全断开与原对象的引用关系

  • 结果:无论怎么修改 data1,都不会影响 users

  • 局限性JSON 方法):

    • ❌ 无法处理函数、undefinedSymbol
    • ❌ 无法处理循环引用(会报错)
    • ❌ 会丢失原型链、Date 变成字符串等

✅ 更健壮的深拷贝方案:

  • 使用 Lodash:_.cloneDeep(obj)
  • 自己实现递归拷贝(需处理各种边界情况)

三、对比总结

类型是否新内存顶层独立嵌套对象独立适用场景
引用拷贝❌ 否只读、性能敏感
浅拷贝✅ 是(外层)一层结构数据
深拷贝✅ 是(全层)完全隔离、复杂嵌套

四、大厂高频面试题

🔹 Q1:const data = users; 修改 data 会影响 users 吗?为什么?

:会。因为这是引用拷贝,datausers 指向堆中同一个数组对象。const 只保证变量引用地址不变,不阻止对象内容被修改。


🔹 Q2:[...users] 是深拷贝吗?

:不是,是浅拷贝。它只复制了数组的第一层,内部对象仍是共享引用。


🔹 Q3:JSON.parse(JSON.stringify(obj)) 有什么缺陷?

  • 无法拷贝函数、undefinedSymbol
  • Date 对象会变成字符串
  • 正则、Error 等特殊对象会变成空对象 {}
  • 遇到循环引用会抛出异常

🔹 Q4:如何实现一个可靠的深拷贝函数?

参考答案(简化版):

js
编辑
function deepClone(obj, hash = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  if (hash.has(obj)) return hash.get(obj); // 处理循环引用

  const cloned = new obj.constructor();
  hash.set(obj, cloned);

  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      cloned[key] = deepClone(obj[key], hash);
    }
  }
  return cloned;
}

五、结语

理解 引用、浅拷贝、深拷贝 的本质,不仅是写出健壮代码的基础,更是深入掌握 JavaScript 内存模型的必经之路。在实际开发中:

  • 若数据无嵌套 → 用浅拷贝(简洁高效)
  • 若需完全隔离 → 用深拷贝(注意边界情况)
  • 切勿混淆 const 与“不可变性”——它只锁住引用,不锁内容!

掌握这些知识,你离大厂 Offer 又近了一步!🚀


📚 延伸阅读建议

  • V8 引擎内存管理机制
  • Immutable.js / Immer 库的不可变更新思想
  • 结构化克隆算法(Structured Clone Algorithm)——浏览器内置的深拷贝机制(用于 postMessage 等)