JS的深浅拷贝

今天记录一些JS中涉及深浅拷贝的函数,以及手写深拷贝的方法。

对象浅拷贝

  1. Object.assign({},obj1)
JavaScript 复制代码
const obj1 = { a: 1, b: 2 };
const obj2 = Object.assign({}, obj1);
console.log(obj2); // { a: 1, b: 2 }
  1. 展开运算符
JavaScript 复制代码
const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1 };
console.log(obj2); // { a: 1, b: 2 }
  1. for in
JavaScript 复制代码
const obj1 = { 
  a: 1, 
  b: 2, 
  c: { d: 4 }  // 嵌套对象
};

const obj2 = {}; // 空对象

for (const key in obj1) {
  if (obj1.hasOwnProperty(key)) {
    obj2[key] = obj1[key]; // 复制属性
  }
}

console.log(obj2); // 输出: { a: 1, b: 2, c: { d: 4 } }
复制代码
之所以要进行hasOwnProperty()判断,是因为for in也会遍历对象原型链上的属性,所以要过滤掉它们
  1. Object.keys(obj).forEach()
JavaScript 复制代码
const obj1 = { a: 1, b: 2, c: 3 };  // 原始对象
const obj2 = {};                    // 目标空对象

// 使用Object.keys和forEach复制属性
Object.keys(obj1).forEach((key) => {
    obj2[key] = obj1[key];  // 蓝色高亮的关键操作行
});

console.log(obj2);  // 输出: { a: 1, b: 2, c: 3 }
复制代码
遍历对象所有的key,然后拷贝

对象深拷贝

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

    先用JSON.stringify(obj)把对象转化为JSON字符串

    然后再用JSON.parse()转化回对象

JavaScript 复制代码
const obj = {
  name: "John",
  age: 30,
  address: {
    city: "New York",
    country: "USA"
  }
};

// 使用 JSON 方法实现深拷贝
const deepCopy = JSON.parse(JSON.stringify(obj));

console.log(deepCopy); // 输出: { name: 'John', age: 30, address: { city: 'New York', country: 'USA' } }
复制代码
- 缺点:
    1. 无法拷贝函数(JSON在序列化时会忽略函数)
JavaScript 复制代码
const obj = {
  a: 1,
  b: function() {
    console.log("hello");
  }
};

const newObj = JSON.parse(JSON.stringify(obj));
console.log(newObj); // { a: 1 },函数没有被拷贝
复制代码
    2. 无法拷贝特殊对象,如Date、正则表达式
JavaScript 复制代码
const obj = {
  date: new Date(),      // Date对象
  regex: /test/         // 正则表达式
};

const newObj = JSON.parse(JSON.stringify(obj));

console.log(newObj.date);  // 输出ISO格式字符串,而不是Date对象(如"2023-05-15T12:00:00.000Z")
console.log(newObj.regex); // 输出空对象 {}
复制代码
    3. 无法拷贝原型链上的属性
TypeScript 复制代码
const protoObj = { c: 3 };  // 原型对象

// 创建以 protoObj 为原型的对象
const obj = Object.create(protoObj);
obj.a = 1;  // 添加实例属性
obj.b = 2;

// 通过 JSON 序列化/反序列化创建新对象
const newObj = JSON.parse(JSON.stringify(obj));

console.log(newObj.c);  // 输出: undefined
复制代码
        obj的原型对象是protoObj
    4. 会忽略symbol和undefined属性
JavaScript 复制代码
const obj = {
  a: undefined,          // undefined值
  b: Symbol('test')      // Symbol值
};

const newObj = JSON.parse(JSON.stringify(obj));
console.log(newObj);     // 输出: {}

数组浅拷贝

  1. arr.slice()
JavaScript 复制代码
const arr1 = [1, 2, 3];          // 原始数组
const arr2 = arr1.slice();       // 使用slice()创建浅拷贝
console.log(arr2);               // 输出: [1, 2, 3]
复制代码
arr.slice(2,3):截取arr数组索引在[2,3)这个区间的值,返回新数组
  1. [].concat(arr)
JavaScript 复制代码
const arr1 = [1, 2, 3];        // 原始数组
const arr2 = [].concat(arr1);  // 使用空数组concat方法创建浅拷贝
console.log(arr2);             // 输出: [1, 2, 3]

数组深拷贝

  1. JSON.parse(JSON.stringify())
JavaScript 复制代码
const arr = [1, {a: 2}, [3]];
const deepCopy = JSON.parse(JSON.stringify(arr));
复制代码
缺点:

- 会丢失 `undefined`、`Function`、`Symbol` 等特殊值
- 无法处理循环引用(会报错)
- `Date` 对象会转为字符串,`RegExp` 会变成空对象
  1. Lodash 的 _.cloneDeep()
JavaScript 复制代码
import _ from 'lodash';
const arr = [{a: 1}, new Date()];
const deepCopy = _.cloneDeep(arr);
复制代码
**特点**:
  - 支持所有标准JS类型
  - 自动处理循环引用

手写深拷贝

简易深拷贝

JavaScript 复制代码
function deepClone(source) {
  if(typeof source != "object" || source == null){
    return source;
  }
  const target = Array.isArray(source) ? [] : {};
  
  // 无论是[]还是{},都可以通过for in遍历所有属性
  for(const key in source){
    // 如果当前所遍历的属性是object,则递归调用deepClone
    // 如果当前的属性是基本类型,则直接返回
    if(typeof source[key] === "object" && source[key] != null) {
      target[key] = deepClone(source[key]);
    }else{
      target[key] = source[key];
    }
  }
  return target;
}

const original = {
  number: 123,
  string: "hello",
  array: [1,2,3],
  obj: {
    prop1:"test",
    prop2: {
      nested: "test2",
    },
  },
};

const cloned = deepClone(original)

console.log(cloned);

不过这个还是有一些bug,比如循环引用的问题:

JavaScript 复制代码
const obj = { 
  a: 1, 
  b: { 
    c: 2 
  } 
};
obj.self = obj;  // 创建循环引用
const copy = deepClone(obj);  // 深拷贝操作
console.log(copy);  // 输出克隆结果

因为obj.self() = obj会导致循环引用,在克隆时会无限调用,会出现栈溢出的情况

解决循环引用问题

可以使用map解决循环依赖的问题

JavaScript 复制代码
function deepClone(source, clonedMap = new Map()) {
  if(typeof source != "object" || source == null){
    return source;
  }
  
  // 如果这个对象已经被拷贝过,直接从Map中返回
  if(clonedMap.has(source)){
    return clonedMap.get(source);
  }
  
  const target = Array.isArray(source) ? [] : {};
  
  // 在Map中记录这个对象
  clonedMap.set(source, target);
  
  for(const key in source){
    if(typeof source[key] === "object" && source[key] != null) {
      target[key] = deepClone(source[key],clonedMap);
    }else{
      target[key] = source[key];
    }
  }
  return target;
}

const original = {
  number: 123,
  string: "hello",
  array: [1,2,3],
  obj: {
    prop1:"test",
    prop2: {
      nested: "test2",
    },
  },
};

const cloned = deepClone(original)

console.log(cloned);

现在仍然存在一些问题:

  • Date对象无法拷贝
  • 正则表达式无法拷贝
JavaScript 复制代码
// 日期对象深拷贝示例
const originalDateObj = {
  date: new Date()  // 创建Date对象
};
const clonedDateObj = deepClone(originalDateObj);
console.log(originalDateObj.date); // 原始的Date对象
console.log(clonedDateObj.date);   // 拷贝的Date对象

// 正则表达式对象深拷贝示例
const originalRegExpObj = {
  regExp: /abc/gi  // 创建正则表达式
};
const clonedRegExpObj = deepClone(originalRegExpObj);
console.log(originalRegExpObj.regExp); // 原始的正则表达式对象
console.log(clonedRegExpObj.regExp);   // 拷贝后的正则表达式对象

支持拷贝Date和正则表达式:

所以为什么无法拷贝Date和正则表达式?

问题就出在这一句上

我们只是判断了当前元素是否是一个数组,所以如果当前对象是Date或正则表达式,我们只会创建一个{}

这是不行的,我们需要更细致的分类讨论:

JavaScript 复制代码
if(Array.isArray(source)){
  target = [];
}else if(source instanceof Date){
  target = new Date(source);
}else if(source instanceof RegExp){
  target = new RegExp(source.source,source.flags);
}else{
  target = {};
}

完整版:

JavaScript 复制代码
function deepClone(source, clonedMap = new Map()) {
  if(typeof source != "object" || source == null){
    return source;
  }
  
  // 如果这个对象已经被拷贝过,直接从Map中返回
  if(clonedMap.has(source)){
    return clonedMap.get(source);
  }
  
  if(Array.isArray(source)){
    target = [];
  }else if(source instanceof Date){
    target = new Date(source);
  }else if(source instanceof RegExp){
    target = new RegExp(source.source,source.flags);
  }else{
    target = {};
  }
  
  // 在Map中记录这个对象
  clonedMap.set(source, target);
  
  for(const key in source){
    if(typeof source[key] === "object" && source[key] != null) {
      target[key] = deepClone(source[key],clonedMap);
    }else{
      target[key] = source[key];
    }
  }
  return target;
}

const original = {
  number: 123,
  string: "hello",
  array: [1,2,3],
  obj: {
    prop1:"test",
    prop2: {
      nested: "test2",
    },
  },
};

const cloned = deepClone(original)

console.log(cloned);

此时再来试一试:

JavaScript 复制代码
// 日期对象深拷贝示例
const originalDateObj = {
  date: new Date()  // 创建Date对象
};
const clonedDateObj = deepClone(originalDateObj);
console.log(originalDateObj.date); // 原始的Date对象
console.log(clonedDateObj.date);   // 拷贝的Date对象

// 正则表达式对象深拷贝示例
const originalRegExpObj = {
  regExp: /abc/gi  // 创建正则表达式(带g和i标志)
};
const clonedRegExpObj = deepClone(originalRegExpObj);
console.log(originalRegExpObj.regExp); // 原始的正则表达式对象
console.log(clonedRegExpObj.regExp);   // 拷贝后的正则表达式对象

其实这个代码还是有问题的,它无法拷贝Symbol

支持拷贝Symbol类型

那么为什么拷贝不了Symbol类型属性?

问题就出在for in循环,for in循环是无法遍历Symbol类型的

所以在for in结束后应该单独判断一下所有的Symbol类型属性

JavaScript 复制代码
const symbolKeys = Object.getOwnPropertySymbols(source)
for(const symKey of symbolKeys){
  target[symmKey] = deepClone(source[symKey],clonedMap);
}

完整版:

JavaScript 复制代码
function deepClone(source, clonedMap = new Map()) {
  // 如果是基本数据类型则直接返回,否则走深拷贝步骤
  if(typeof source != "object" || source == null){
    return source;
  }
  
  // 如果Map中已经有当前对象了,则直接返回(防止循环引用导致栈溢出)
  if(clonedMap.has(source)){
    return clonedMap.get(source);
  }
  
  // 判断四种类型:对象、数组、Date、正则表达式
  if(Array.isArray(source)){
    target = [];
  }else if(source instanceof Date){
    target = new Date(source);
  }else if(source instanceof RegExp){
    target = new RegExp(source.source,source.flags);
  }else{
    target = {};
  }
  
  // 在Map中记录这个对象
  clonedMap.set(source, target);
  
  // 遍历属性,进行深拷贝
  for(const key in source){
    if(typeof source[key] === "object" && source[key] != null) {
      target[key] = deepClone(source[key],clonedMap);
    }else{
      target[key] = source[key];
    }
  }
  
  // 克隆Symbol属性
  const symbolKeys = Object.getOwnPropertySymbols(source)
  for(const symKey of symbolKeys){
    target[symmKey] = deepClone(source[symKey],clonedMap);
  }
  
  return target;
}

const original = {
  number: 123,
  string: "hello",
  array: [1,2,3],
  obj: {
    prop1:"test",
    prop2: {
      nested: "test2",
    },
  },
};

const cloned = deepClone(original)

console.log(cloned);

注意:

这个深拷贝,原型链上的属性也会拷贝,如果不想拷贝,那就要在for in循环时判断一下,如果属于自身属性才进行下一步:

JavaScript 复制代码
function deepClone(target, source) {
  for (const key in source) {
    // 判断是否属于自身的属性 
    if (Object.hasOwnProperty.call(source, key)) {
      if (typeof source[key] === 'object' && source[key] !== null) {
        target[key] = deepClone({}, source[key]);  
      } else {
        target[key] = source[key];  
      }
    }
  }
  return target;
}
相关推荐
安分小尧1 小时前
React 文件上传新玩法:Aliyun OSS 加持的智能上传组件
前端·react.js·前端框架
编程社区管理员1 小时前
React安装使用教程
前端·react.js·前端框架
拉不动的猪2 小时前
vue自定义指令的几个注意点
前端·javascript·vue.js
yanyu-yaya2 小时前
react redux的学习,单个reducer
前端·javascript·react.js
skywalk81632 小时前
OpenRouter开源的AI大模型路由工具,统一API调用
服务器·前端·人工智能·openrouter
Liudef062 小时前
deepseek v3-0324 Markdown 编辑器 HTML
前端·编辑器·html·deepseek
拉不动的猪2 小时前
uniapp与React Native/vue 的简单对比
前端·vue.js·面试
加瓦点灯2 小时前
观察者模式:解耦对象间的依赖关系
开发语言·javascript·观察者模式
z_mazin3 小时前
Chrome开发者工具实战:调试三剑客
前端·javascript·chrome·网络爬虫
sen_shan4 小时前
Vue3+Vite+TypeScript+Element Plus开发-04.静态菜单设计
前端·javascript·typescript·vue3·element·element plus·vue 动态菜单