出处:掘金

原作者:金泽宸


为什么要掌握深拷贝?

  • JS 中对象赋值是引用赋值,不是值复制
  • JSON.parse(JSON.stringify()) 有诸多缺陷(如丢失函数、正则、undefined、循环引用会报错)
  • 项目中常需要完整复制一份对象结构,避免原始数据被意外修改
  • 面试中常考实现 + 处理边界

深拷贝 VS. 浅拷贝区别

​​特性​​​浅拷贝 (Shallow Clone)​​​深拷贝 (Deep Clone)​​
​复制深度​仅复制对象的第一层属性递归复制所有嵌套层级
​引用类型处理​复制引用地址(新旧对象共享嵌套对象)创建全新引用(完全分离嵌套对象)
​内存占用​低(共享嵌套对象)高(创建所有层级副本)
​修改影响​修改嵌套属性会影响原对象修改不影响原对象
​适用场景​简单对象、无嵌套引用复杂嵌套对象、需要完全隔离的场景
工具Object.assign、展开运算符自定义函数、structuredClone(现代浏览器)

浅拷贝实现方法

Object.assign

Object.assign 是 ES6 中 Object 的一个静态方法,该方法可以用于 JS 对象的合并等多个用途,其中一个用途就是可以进行浅拷贝。该方法的第一个参数是拷贝的目标对象,后面的参数是拷贝的来源对象(可以有多个来源)

Object.assign(target, ...sources)

示例代码:

let target = {};
let source = { a: { b: 1 } };
Object.assign(target, source);
console.log(target); // { a: { b: 1 } };

但是使用 object.assign 方法有几点需要注意:

  • 它不会拷贝对象的继承属性
  • 它不会拷贝对象的不可枚举的属性
  • 可以拷贝 Symbol 类型的属性
let obj1 = { a: { b:1 }, sym: Symbol(1) }; 
Object.defineProperty(obj1, 'innumerable' ,{
    value: '不可枚举属性',
    enumerable: false
});
let obj2 = {};
Object.assign(obj2, obj1)
obj1.a.b = 2;
console.log('obj1', obj1); // { a: { b: 2 }, sym: Symbol(1), innumerable: '不可枚举属性' }
console.log('obj2', obj2); // { a: { b: 2 }, sym: Symbol(1) }

从上面的样例代码中可以看到,利用 object.assign 可以拷贝 Symbol,不可以拷贝不可枚举的属性,但是如果到了对象的第二层属性 obj1.a.b,前者值的改变也会影响后者的第二层属性的值,说明其中依旧存在着访问共同堆内存的问题,也就是说这种方法还不能进一步复制,而只是完成了浅拷贝的功能

扩展运算符

可以利用 ES6 的扩展运算符,在构造对象的同时完成浅拷贝的功能。扩展运算符的语法为:

const cloneObj = { ...sourcesObj1, ...sourcesObj2 }
 
const cloneArr = [ ...sourcesArr1, ...sourcesArr2 ]

扩展运算符和 object.assign 有同样的缺陷,也就是实现的浅拷贝的功能差不多,但是如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便

concat 拷贝数组

数组的 concat 方法也是浅拷贝,所以连接一个含有引用类型的数组时,需要注意修改原数组中的元素的属性,因为它会影响拷贝之后连接的数组

const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const newArr = arr1.concat(arr2);
newArr[1] = 100;
console.log(arr1);   // [1, 2, 3]
console.log(arr2);   // [4, 5, 6]
console.log(newArr); // [1, 100, 3, 4, 5, 6]

slice 拷贝数组

数组的 slice 方法也是浅拷贝,slice 方法会返回一个新的数组对象,这一对象由该方法的前两个参数来决定原数组截取的开始和结束时间,是不会影响和改变原始数组的

arr.slice(begin, end);
const arr = [1, 2, { val: 4 }];
const newArr = arr.slice();
newArr[2].val = 1000;
console.log(arr);  // [ 1, 2, { val: 1000 } ]

手写

for…in 循环会遍历对象的所有可枚举属性包括其原型链(prototype chain)上的属性。而 hasOwnProperty 的作用是判断属性是否为对象自身的属性(而非继承的)

for…of 只能遍历实现了迭代器接口Symbol.iterator)的可迭代对象(如数组、字符串、Map、Set 等),但普通对象默认没有实现该接口,直接使用会报错

const obj = { a: 1, b: 2 };
for (const value of obj) {} // TypeError: obj is not iterable 
function shallowClone(target) {
  // 1. 判断是否是对象或数组(非原始类型)
  if (typeof target === "object" && target !== null) {
    // 2. 根据类型创建空数组或空对象
    const cloneTarget = Array.isArray(target) ? [] : {};
    
    // 3. 遍历对象的自有属性(不遍历原型链上的属性)
    for (let prop in target) {
      if (target.hasOwnProperty(prop)) {
        // 4. 直接复制属性值(浅拷贝)
        cloneTarget[prop] = target[prop];
      }
    }
    return cloneTarget;
  } else {
    // 5. 如果是原始类型(如数字、字符串),直接返回
    return target;
  }
}

深拷贝

浅拷贝只是创建了一个新的对象,复制了原有对象的基本类型的值,而引用数据类型只拷贝了一层属性,再深层的还是无法进行拷贝。深拷贝则不同,对于复杂引用数据类型,其在堆内存中完全开辟了一块内存地址,并将原有的对象完全复制过来存放

这两个对象是相互独立、不受影响的,彻底实现了内存上的分离。总的来说,深拷贝的原理可以总结如下:

将一个对象从内存中完整地拷贝出来一份给目标对象,并从堆内存中开辟一个全新的空间存放新对象,且新对象的修改并不会改变原对象,二者实现真正的分离

核心难点

  1. 递归嵌套结构
  2. 处理循环引用
  3. 正确处理各种类型(Date、RegExp、Set、Map、Function、Symbol、BigInt、null、undefined)

JSON 实现深拷贝

JSON.stringify() 是目前开发过程中最简单的深拷贝方法,其实就是把一个对象序列化成为 JSON 的字符串,并将对象里面的内容转换成字符串,最后再用 JSON.parse() 的方法将 JSON 字符串生成一个新的对象

const obj = { a: 1, b: { c: 2 } };
const cloned = JSON.parse(JSON.stringify(obj));
obj.b.c = 3;
console.log(cloned.b.c); // 2(原对象修改不影响克隆对象)

无法克隆函数、RegExp 等特殊对象

  • 原因:JSON 格式仅支持基础类型(字符串、数字、布尔、null)和纯数据结构(对象、数组),无法表示函数、RegExp、Date、Map、Set 等特殊对象
  • 函数、undefined、Symbol:直接忽略(属性丢失)
  • RegExp、Set、Map、Error:序列化为空对象 {},丢失正则表达式规则和标志,丢失 Set、Map 的内容
  • Date:被转换为 ISO 格式的字符串,解析后仍是字符串而非 Date 实例
  • NaN、Infinity:转为 null
// 函数、undefined、Symbol:直接忽略(属性丢失)
const obj = { func: Object.assign, a: undefined, symbol: Symbol('test') };
const cloned = JSON.parse(JSON.stringify(obj));
console.log(cloned); // {}
 
// RegExp、Set、Map、Error:序列化为空对象 {}
const obj = { regex: /\d+/g, map: new Map([['a', 1]]), set: new Set([1, 2, 3]), err: new Error('test errot') };
const cloned = JSON.parse(JSON.stringify(obj));
console.log(cloned); // { regex: {}, map: {}, set: {}, err: {} }
 
// Date:被转换为 ISO 格式的字符串,解析后仍是字符串而非 Date 实例
const obj = { date: new Date() };
const cloned = JSON.parse(JSON.stringify(obj));
console.log(cloned.date); // "2025-03-08T00:00:00.000Z"
 
// NaN、Infinity:转为 null
const obj = { nan: NaN, inf: Infinity };
const cloned = JSON.parse(JSON.stringify(obj));
console.log(cloned); // { nan: null, inf: null }

抛弃对象的原型链

抛弃对象的 constructor,所有构造函数指向 Object

  • 原因:JSON.parse 生成的始终是普通对象 {},无法保留原对象的原型链和构造函数信息
  • 例如:原对象是自定义类 Person 的实例,克隆后的对象变成普通对象,constructor 指向 Object,而非 Person
  • 这会破坏 instanceof 检查和依赖原型链的方法

无法处理循环引用

const obj = { a: 1 };
obj.self = obj;
JSON.parse(JSON.stringify(obj)); // Uncaught TypeError: Converting circular structure to JSON

现代浏览器方案:structuredClone

const newObj = structuredClone(obj); // 现代浏览器支持,内建处理循环引用、Map、Set 等

缺点:

  • 不支持 function、DOM 节点等
  • 兼容性较差(IE、老版本 Safari)

手写递归实现

基础版:不支持循环引用

通过 for…in 遍历传入参数的属性值,如果值是引用类型则再次递归调用该函数,如果是基础数据类型就直接复制

function deepClone(obj) {
  if (obj === null || typeof obj !== 'object') return obj;
 
  const result = Array.isArray(obj) ? [] : {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      result[key] = deepClone(obj[key]);
    }
  }
  return result;
}

进阶版:支持循环引用 & 多类型处理

function deepClone(obj, hash = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj;
 
  // 如果对象已被拷贝过,直接返回缓存值(处理循环引用)
  if (hash.has(obj)) return hash.get(obj);
 
  // 特殊对象类型处理
  const type = Object.prototype.toString.call(obj);
 
  // 日期
  if (type === '[object Date]') return new Date(obj);
  // 正则
  if (type === '[object RegExp]') return new RegExp(obj.source, obj.flags);
  // Map
  if (type === '[object Map]') {
    const map = new Map();
    hash.set(obj, map);
    obj.forEach((v, k) => map.set(deepClone(k, hash), deepClone(v, hash)));
    return map;
  }
  // Set
  if (type === '[object Set]') {
    const set = new Set();
    hash.set(obj, set);
    obj.forEach(v => set.add(deepClone(v, hash)));
    return set;
  }
 
  // 普通对象/数组
  const result = Array.isArray(obj) ? [] : {};
  hash.set(obj, result);
 
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      result[key] = deepClone(obj[key], hash);
    }
  }
 
  return result;
}

高级版:保证对象的原型不丢失

const getType = obj => Object.prototype.toString.call(obj);
 
const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null;
 
// 可迭代
const canTraverse = {
  '[object Map]': true,
  '[object Set]': true,
  '[object Array]': true,
  '[object Object]': true,
  '[object Arguments]': true,
};
const mapTag = '[object Map]';
const setTag = '[object Set]';
const boolTag = '[object Boolean]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';
 
// 处理正则
const handleRegExp = (target) => {
  const { source, flags } = target;
  return new target.constructor(source, flags);
}
 
// 处理函数
const handleFunc = (func) => {
  // 箭头函数直接返回自身
  if(!func.prototype) return func;
 
  const bodyReg = /(?<={)(.|\n)+(?=})/m;
  const paramReg = /(?<=\().+(?=\)\s+{)/;
  const funcString = func.toString();
  // 分别匹配 函数参数 和 函数体
  const param = paramReg.exec(funcString);
  const body = bodyReg.exec(funcString);
  if(!body) return null;
  if (param) {
    const paramArr = param[0].split(',');
    return new Function(...paramArr, body[0]);
  } else {
    return new Function(body[0]);
  }
}
 
const handleNotTraverse = (target, tag) => {
  const Ctor = target.constructor;
  switch(tag) {
    case boolTag:
      return new Object(Boolean.prototype.valueOf.call(target));
    case numberTag:
      return new Object(Number.prototype.valueOf.call(target));
    case stringTag:
      return new Object(String.prototype.valueOf.call(target));
    case symbolTag:
      return new Object(Symbol.prototype.valueOf.call(target));
    case errorTag: 
    case dateTag:
      return new Ctor(target);
    case regexpTag:
      return handleRegExp(target);
    case funcTag:
      return handleFunc(target);
    default:
      return new Ctor(target);
  }
}
 
const deepClone = (target, map = new WeakMap()) => {
  if(!isObject(target)) 
    return target;
  let type = getType(target);
  let cloneTarget;
  if (!canTraverse[type]) {
    // 处理不能遍历的对象
    return handleNotTraverse(target, type);
  } else {
    // 这波操作相当关键,可以保证对象的原型不丢失!
    let ctor = target.constructor;
    cloneTarget = new ctor();
  }
 
  if (map.get(target)) 
    return target;
  map.set(target, true);
 
  if(type === mapTag) {
    //处理Map
    target.forEach((item, key) => {
      cloneTarget.set(deepClone(key, map), deepClone(item, map));
    })
  }
  
  if(type === setTag) {
    //处理Set
    target.forEach(item => {
      cloneTarget.add(deepClone(item, map));
    })
  }
 
  // 处理数组和对象
  for (let prop in target) {
    if (target.hasOwnProperty(prop)) {
        cloneTarget[prop] = deepClone(target[prop], map);
    }
  }
  return cloneTarget;
}

面试常见加分点

问题解法
如何处理循环引用?使用 WeakMap
为什么不用 JSON 序列化?会丢失函数、undefined、循环引用报错
如何处理特殊对象?判断 Object.prototype.toString.call(obj) 的类型