Js中浅拷贝与深拷贝

268 阅读6分钟

前言

无论你在哪个平台学习 JavaScript,深浅拷贝都是绕不开的核心知识点。它直接关系到对象数据的修改安全,也是面试高频考点。本文将从 “拷贝的本质” 出发,清晰拆解浅拷贝与深拷贝的区别、实现方式及适用场景,帮你彻底搞懂这一基础概念。

一、浅拷贝:只拷贝对象第一层

浅拷贝的操作对象仅为引用类型(如对象、数组)  。它对对象第一层的处理规则如下:

  • 若属性是基本数据类型(number、string 等)  ,直接拷贝值,修改新对象不会影响原对象。
  • 若属性是引用数据类型(对象、数组等)  ,仅拷贝引用地址,新对象与原对象的嵌套引用指向同一内存地址,修改嵌套内容会相互影响。

浅拷贝实现方式

方式 1:Object.assign ()(ES5 方法)

Object.assign(target, source) 会将源对象(source)的可枚举属性复制到目标对象(target),返回目标对象。

// 原对象(包含基本类型和引用类型属性)
const originalObj = { 
  name: "张三",  // 基本类型
  info: { age: 20 }  // 引用类型
};
// 目标对象(初始空对象)
const copyObj = {};
// 执行浅拷贝
Object.assign(copyObj, originalObj);

// 验证:修改基本类型属性(互不影响)
copyObj.name = "李四";
console.log(originalObj.name); // 输出 "张三"(原对象不变)

// 验证:修改引用类型属性(相互影响)
copyObj.info.age = 22;
console.log(originalObj.info.age); // 输出 22(原对象被修改)

方式 2:扩展运算符 ...(ES6 方法)

扩展运算符可快速拷贝对象或数组的第一层,语法更简洁,是日常开发的首选。

// 1. 拷贝对象
const originalObj = { name: "张三", info: { age: 20 } };
const copyObj = { ...originalObj }; // 浅拷贝

// 验证引用类型关联
copyObj.info.age = 22;
console.log(originalObj.info.age); // 输出 22(原对象被修改)

// 2. 拷贝数组
const originalArr = [1, { value: 10 }];
const copyArr = [...originalArr]; // 浅拷贝

// 验证引用类型关联
copyArr[1].value = 20;
console.log(originalArr[1].value); // 输出 20(原数组被修改)

方式 3:数组的 slice () 方法

slice(start, end) 用于截取数组片段,返回新数组,本质是对数组的浅拷贝(仅作用于数组)。

const originalArr = [{ name: "苹果" }, { name: "香蕉" }];
const copyArr = originalArr.slice(); // 不传参数时,拷贝整个数组

// 验证引用类型关联
copyArr[0].name = "西瓜";
console.log(originalArr[0].name); // 输出 "西瓜"(原数组被修改)

二、深拷贝:完全独立的新对象

深拷贝会递归拷贝对象的所有层级(包括嵌套的引用类型),最终生成一个与原对象完全独立的新对象。修改新对象的任何属性(无论层级),都不会影响原对象。

深拷贝实现方式(附完整代码)

方式 1:JSON.parse (JSON.stringify ())(简单场景适用)

通过 “先转 JSON 字符串,再转对象” 的方式实现深拷贝,无需额外依赖,但存在明显局限性。

const originalObj = { 
  name: "张三", 
  info: { age: 20 },
  hobby: ["游戏", "运动"]
};
// 执行深拷贝
const copyObj = JSON.parse(JSON.stringify(originalObj));

// 验证:修改嵌套引用类型(互不影响)
copyObj.info.age = 22;
copyObj.hobby[0] = "阅读";
console.log(originalObj.info.age); // 输出 20(原对象不变)
console.log(originalObj.hobby[0]); // 输出 "游戏"(原对象不变)

⚠️ 局限性(必须注意)

  • 无法处理 function 类型:会直接忽略函数属性

    const obj = { fn: () => console.log("hello") };
    const copy = JSON.parse(JSON.stringify(obj));
    console.log(copy.fn); // 输出 undefined(函数被忽略)
    
  • 无法处理 undefined 类型:会忽略值为 undefined 的属性

    const obj = { age: undefined };
    const copy = JSON.parse(JSON.stringify(obj));
    console.log(copy.age); // 输出 undefined(属性被忽略,copy 仅为 {})
    
  • 无法处理循环引用:会直接报错(对象属性引用自身)

    const obj = { name: "张三" };
    obj.self = obj; // 循环引用:obj 的 self 属性指向自身
    const copy = JSON.parse(JSON.stringify(obj)); // 报错:TypeError: Converting circular structure to JSON
    

方式 2:第三方库(生产环境首选)

开发中推荐使用成熟的第三方库实现深拷贝,兼容性好、无漏洞,常用库包括 lodash 和 jQuery

以 lodash.cloneDeep 为例(最常用):

// 1. 先安装 lodash(npm 项目)
// npm install lodash

// 2. 引入并使用
const _ = require("lodash");
const originalObj = { 
  name: "张三", 
  info: { age: 20 },
  fn: () => console.log("hello"), // 包含函数
  self: null // 后续用于循环引用
};
originalObj.self = originalObj; // 循环引用

// 执行深拷贝(支持函数、循环引用)
const copyObj = _.cloneDeep(originalObj);

// 验证:完全独立
copyObj.info.age = 22;
console.log(originalObj.info.age); // 输出 20(原对象不变)
console.log(copyObj.fn); // 输出 [Function: fn](函数被保留)

方式 3:手写深拷贝(面试高频考点)

自定义深拷贝函数可根据需求灵活扩展,核心是 “递归遍历 + 处理不同数据类型 + 解决循环引用”。

function deepClone(original) {
  // 1. 存储已拷贝的对象,解决循环引用(用 WeakMap 避免内存泄漏)
  const cache = new WeakMap();

  // 2. 递归拷贝的核心函数
  const _clone = (value) => {
    // 情况 1:非对象类型(基本类型、null、undefined),直接返回值
    if (value === null || typeof value !== "object") {
      return value;
    }

    // 情况 2:已拷贝过的对象,直接返回缓存结果(解决循环引用)
    if (cache.has(value)) {
      return cache.get(value);
    }

    let result;
    // 情况 3:数组类型
    if (Array.isArray(value)) {
      result = [];
      cache.set(value, result); // 先缓存,再递归(避免循环引用)
      value.forEach((item, index) => {
        result[index] = _clone(item); // 递归拷贝数组元素
      });
    }
    // 情况 4:普通对象类型(排除数组、Date、Map 等)
    else if (Object.prototype.toString.call(value) === "[object Object]") {
      result = {};
      cache.set(value, result); // 先缓存,再递归
      for (const key in value) {
        // 只拷贝对象自身的可枚举属性(跳过原型链属性)
        if (value.hasOwnProperty(key)) {
          result[key] = _clone(value[key]); // 递归拷贝属性值
        }
      }
    }
    // 情况 5:扩展支持 Date 类型(可选,根据需求添加)
    else if (value instanceof Date) {
      result = new Date(value); // Date 对象直接新建
      cache.set(value, result);
    }

    return result;
  };

  return _clone(original);
}

// 测试手写深拷贝
const originalObj = { 
  name: "张三", 
  info: { age: 20 },
  time: new Date() // 包含 Date 类型
};
originalObj.self = originalObj; // 循环引用

const copyObj = deepClone(originalObj);
copyObj.info.age = 22;
console.log(originalObj.info.age); // 输出 20(完全独立)
console.log(copyObj.time instanceof Date); // 输出 true(Date 类型保留)

三、深浅拷贝核心区别对比

对比维度浅拷贝深拷贝
拷贝层级仅拷贝对象第一层递归拷贝所有层级
引用类型处理拷贝引用地址,共享内存拷贝值,完全独立内存
修改影响嵌套引用类型修改会相互影响任何修改都互不影响
实现复杂度简单(无需递归)复杂(需递归 + 处理特殊类型)
适用场景简单对象、无嵌套引用类型复杂对象、有嵌套引用类型

总结

深浅拷贝的核心差异在于 “是否处理嵌套引用类型”:

  • 日常开发中,若对象仅一层结构,优先用 扩展运算符 实现浅拷贝,高效简洁;
  • 若对象包含多层嵌套(如复杂表单、树形数据),推荐用 lodash.cloneDeep 实现深拷贝,避免手动写函数的漏洞;
  • 面试时,需能手写基础深拷贝函数,并说明 “循环引用” 和 “特殊类型(Date、Map)” 的处理思路。

学习深浅拷贝不仅是掌握语法,更能帮你理解 JavaScript 中 “引用类型” 的内存机制,减少开发中的数据修改 bug。