JS的深浅拷贝

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

对象浅拷贝

  1. Object.assign({},obj1)
const obj1 = { a: 1, b: 2 };
const obj2 = Object.assign({}, obj1);
console.log(obj2); // { a: 1, b: 2 }
  1. 展开运算符
const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1 };
console.log(obj2); // { a: 1, b: 2 }
  1. for in
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()
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()转化回对象

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在序列化时会忽略函数)
const obj = {
  a: 1,
  b: function() {
    console.log("hello");
  }
};

const newObj = JSON.parse(JSON.stringify(obj));
console.log(newObj); // { a: 1 },函数没有被拷贝
    2. 无法拷贝特殊对象,如Date、正则表达式
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. 无法拷贝原型链上的属性
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属性
const obj = {
  a: undefined,          // undefined值
  b: Symbol('test')      // Symbol值
};

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

数组浅拷贝

  1. arr.slice()
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)
const arr1 = [1, 2, 3];        // 原始数组
const arr2 = [].concat(arr1);  // 使用空数组concat方法创建浅拷贝
console.log(arr2);             // 输出: [1, 2, 3]

数组深拷贝

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

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

手写深拷贝

简易深拷贝

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,比如循环引用的问题:

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

解决循环引用问题

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

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对象无法拷贝
  • 正则表达式无法拷贝
// 日期对象深拷贝示例
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或正则表达式,我们只会创建一个{}

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

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 = {};
}


完整版:

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);

此时再来试一试:

// 日期对象深拷贝示例
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类型属性

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

完整版:

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循环时判断一下,如果属于自身属性才进行下一步:

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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值