创见博客
js中的浅拷贝与深拷贝
七崽爱吃小饼干2025/12/28阅读 2专栏 JavaScript

一、定义

JS 中拷贝的核心划分是浅拷贝(Shallow Copy) 和 深拷贝(Deep Copy),本质差异在于对「嵌套引用类型数据」的处理方式:

  • 浅拷贝:仅复制数据的「表层结构」,对于嵌套的引用类型(如对象、数组、函数等),只复制其引用地址(而非实际内容),修改拷贝后的嵌套数据,会同步影响原数据。
  • 深拷贝:递归复制数据的「所有层级结构」,嵌套的引用类型会被重新创建并复制内容,拷贝后的数据与原数据完全独立,修改一方不会影响另一方。
  • 补充:JS 中基本类型(string、number、boolean、null、undefined、symbol、bigint)不存在 “浅 / 深拷贝” 之分,因为它们存储在栈内存中,赋值时直接复制值本身。

二、浅拷贝常见实现

1.数组浅拷贝

  • Array.prototype.slice(0):截取整个数组,返回新数组(表层拷贝)
  • Array.prototype.concat():空参数拼接,返回新数组(表层拷贝)
  • 扩展运算符 [...arr]:ES6+ 语法,简洁直观(推荐)
  • Array.from(arr):将类数组 / 数组转为新数组(表层拷贝)
javascript
// 数组浅拷贝示例
const originalArr = [1, 'a', { name: '张三' }, [10, 20]];
// 四种浅拷贝方式
const copyArr1 = originalArr.slice(0);
const copyArr2 = originalArr.concat();
const copyArr3 = [...originalArr];
const copyArr4 = Array.from(originalArr);

// 验证:修改表层基本类型,不影响原数组
copyArr3[0] = 100;
console.log(originalArr[0]); // 输出 1(原数据不变)
console.log(copyArr3[0]);    // 输出 100(拷贝后数据修改)

// 验证:修改嵌套引用类型,影响原数组(浅拷贝核心特征)
copyArr3[2].name = '李四';
copyArr3[3][0] = 1000;
console.log(originalArr[2].name); // 输出 李四(原数据被修改)
console.log(originalArr[3][0]);   // 输出 1000(原数据被修改)

2.对象浅拷贝

  • Object.assign({}, obj):将原对象属性复制到空对象,返回新对象
  • 扩展运算符 {...obj}:ES6+ 语法,简洁直观(推荐)
javascript
// 对象浅拷贝示例
const originalObj = { id: 1, info: { age: 18 }, hobbies: ['篮球'] };
// 两种浅拷贝方式
const copyObj1 = Object.assign({}, originalObj);
const copyObj2 = { ...originalObj };

// 验证:修改表层基本类型,不影响原对象
copyObj2.id = 100;
console.log(originalObj.id); // 输出 1(原数据不变)
console.log(copyObj2.id);    // 输出 100(拷贝后数据修改)

// 验证:修改嵌套引用类型,影响原对象
copyObj2.info.age = 20;
copyObj2.hobbies[0] = '足球';
console.log(originalObj.info.age); // 输出 20(原数据被修改)
console.log(originalObj.hobbies[0]); // 输出 足球(原数据被修改)

三、深拷贝常见实现

1.JSON.parse(JSON.stringify(obj))

这是 JS 中最简洁的深拷贝写法,通过 JSON 序列化与反序列化实现递归拷贝,但存在多个致命缺陷:

javascript
const originalObj = { id: 1, info: { age: 18 }, hobbies: ['篮球'] };
const copyObj = JSON.parse(JSON.stringify(originalObj));

// 验证:修改嵌套数据,不影响原对象
copyObj.info.age = 20;
copyObj.hobbies[0] = '足球';
console.log(originalObj.info.age); // 输出 18(原数据不变)
console.log(originalObj.hobbies[0]); // 输出 篮球(原数据不变)

局限性(不可忽视):

  • 无法拷贝 undefined、function、symbol 类型(序列化时会丢失这些属性);
  • 无法拷贝循环引用的对象(如 obj.self = obj,会直接报错);
  • 无法拷贝 Date、RegExp、Map、Set 等特殊对象(会被转为普通对象 / 字符串,丢失原有特性);
  • 会忽略对象的原型链,拷贝后的对象仅为普通对象,不继承原对象的原型方法。

2.自定义递归函数

通过递归遍历对象 / 数组的所有层级,手动复制每一层的数据,支持处理各种数据类型,规避 JSON 方法的局限性:

javascript
/**
 * 支持自定义对象类型的深克隆函数
 * @param {any} target 待克隆的目标(含自定义对象实例)
 * @param {WeakMap} cache 缓存已克隆对象,解决循环引用
 * @returns {any} 克隆后的新实例(保留原类型和原型方法)
 */
function deepClone(target, cache = new WeakMap()) {
  // 1. 处理基本类型和 null/undefined,直接返回值
  if (target === null || typeof target !== 'object') {
    return target;
  }

  // 2. 处理循环引用:已克隆过则直接返回缓存结果
  if (cache.has(target)) {
    return cache.get(target);
  }

  let result;

  // 3. 处理 JS 原生内置对象(保持原有逻辑)
  if (Array.isArray(target)) {
    result = [];
  } else if (target instanceof Date) {
    result = new Date(target.getTime());
  } else if (target instanceof RegExp) {
    result = new RegExp(target.source, target.flags);
  } 
  // 4. 处理自定义对象类型(核心:保留构造函数类型)
  else {
    // 关键:通过 target.constructor 创建同类型实例,而非普通 Object
    // 这样会自动继承原对象的原型链(含原型方法)
    result = new target.constructor();
  }

  // 5. 缓存当前克隆实例,解决循环引用
  cache.set(target, result);

  // 6. 递归复制所有自有属性(包括可枚举/不可枚举属性)
  // Reflect.ownKeys 能获取所有自有属性(比 Object.keys 更全面)
  Reflect.ownKeys(target).forEach(key => {
    // 递归克隆属性值,赋值给新实例
    result[key] = deepClone(target[key], cache);
  });

  return result;
}

// 测试验证
const originalObj = {
    id: 1,
    name: '张三',
    info: { age: 18 },
    hobbies: ['篮球', '游泳'],
    birth: new Date('2006-01-01'),
    reg: /abc/gi,
    fn: () => console.log('hello'), // 函数直接引用复制(函数本身不可深拷贝,也无需深拷贝)
};
// 添加循环引用
originalObj.self = originalObj;

const copyObj = deepClone(originalObj);
// 修改拷贝后的数据,验证独立性
copyObj.info.age = 20;
copyObj.hobbies[0] = '足球';
copyObj.birth.setFullYear(2007);

console.log(originalObj.info.age); // 18(原数据不变)
console.log(originalObj.hobbies[0]); // 篮球(原数据不变)
console.log(originalObj.birth.getFullYear()); // 2006(原数据不变)
console.log(copyObj.self === copyObj); // true(循环引用处理正常)

3.第三方库实现Loash.cloneDeep

javascript
// 安装 Lodash:npm i lodash
const _ = require('lodash');

const originalObj = { id: 1, info: { age: 18 }, self: null };
originalObj.self = originalObj; // 循环引用

const copyObj = _.cloneDeep(originalObj);
copyObj.info.age = 20;

console.log(originalObj.info.age); // 18(原数据不变)
console.log(copyObj.self === copyObj); // true(循环引用处理正常)

四、总结

  1. JS 拷贝核心分为「浅拷贝」和「深拷贝」,区别在于是否递归复制嵌套引用类型;

  2. 浅拷贝:表层复制,引用共享,修改嵌套数据会影响原数据,优点是高效(推荐:[...arr]、{...obj}、Object.assign);

  3. 深拷贝:递归复制,完全独立,修改拷贝数据不影响原数据(简易写法:JSON.parse(JSON.stringify),有局限性;推荐写法:自定义递归函数、Lodash.cloneDeep);

  4. 深拷贝需要注意的点:

    • Array、Date、RegExp等类型需要单独处理,否则会丢失原有的特性。
    • 要注意循环引用的处理,可以用map缓存已处理的结果
    • 用Array.isArray()判断是否是Array,用instanceof判断是否是Date和RegExp
    • 自定义对象通过new target.constructor()创建新自定义对象,并且通过Reflect.ownKeys(target).forEach遍历对象属性进行属性的递归复制。
    • 最好用WeakMap对象去缓存循环引用,查找时间复杂度是O(1),并且其是弱引用,不会影响垃圾回收机制回收原对象。
  5. 基本类型(string/number 等)无浅 / 深拷贝之分,赋值即复制值本身;

  6. 选型建议:简单表层数据用浅拷贝,复杂嵌套数据用深拷贝(生产环境优先 Lodash)。

评论
0/100