深拷贝with浅拷贝

本文深入探讨了JavaScript中的变量类型,详细解释了基本类型和引用类型的区别。重点介绍了浅拷贝和深拷贝的概念,包括Object.assign()和展开运算符的使用,以及它们在拷贝对象时的局限性。并通过多种方式实现深拷贝,如JSON方法、for...in遍历、Array.prototype.forEach等,对比了各种方法的优缺点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 变量

ECMAScript中的变量包括两种不同数据类型的值:基本类型值和引用类型值。

  • 基本类型值:UndefinedNullBooleanNumberString
  • 引用类型值:ObjectArray

基本数据类型存储在栈内存,存储的是值。复杂数据类型的值存储在堆内存,地址(指向堆中的值)存储在栈内存。当我们把对象赋值给另外一个变量的时候,复制的是地址,指向同一块内存空间,当其中一个对象改变时,另一个对象也会变化。

2. 浅拷贝: 以赋值的形式拷贝引用对象,仍指向同一个地址,修改时原对象也会受到影响

  • Object.assign(target,source1,source2...) 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。注意前面说的是可枚举属性,这是一个介于完全的深拷贝和完全的浅拷贝之间的方法:

❶ 如果我们把它的第一个参数target设置为一个空对象 {},同时保证剩余的源对象sources中的属性类型不包含引用类型,则该方法的返回值就是一个与源对象相同的但并不在同一块内存空间另一个对象,即获得了源对象的深拷贝:

const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };

const returnedTarget = Object.assign(target, source);
returnedTarget.d = 5;
console.log(source);  // { b: 4, c: 5 }
console.log(returnedTarget);  // {{ a: 1, b: 4, c: 5, d: 5 }

如果目标对象中的属性具有相同的键,则属性将被源中的属性覆盖。后来的源的属性将类似地覆盖早先的属性,因此可以用来合并对象。

❷ 但是,如果源对象的属性中包含某个对象,也就是这个属性的值指向某个对象,就像下面这样:

const target = { a: 1, b: 2 };
var source = {
  name: 'apple',
  content: {
    a: 1,
    b: 2
  }
};

const returnedTarget = Object.assign(target, source);
returnedTarget.d = 5;
returnedTarget.content.c = 6;
console.log(source);  // { name: 'apple', content: { a: 1, b: 2, c: 6 } }
console.log(returnedTarget);  // { a: 1, b: 2, name: 'apple', content: { a: 1, b: 2, c: 6 }, d: 5 }

则使用 Object.assign() 时,返回的目标对象中的content属性与源对象source中的content属性指向的同一块内存区域,即对source下的content属性进行了浅拷贝。

  • 展开运算符"..."

❶ 展开运算符和Object.assgin()的拷贝实现是一样的,也是首层深拷贝,所以进行拷贝的时候要慎用。

let arr = [1, 2, 3, 4, 5, 6];
let arr1 = [...arr];
arr1.push(7);
console.log(arr); //[1, 2, 3, 4, 5, 6]
console.log(arr1); //[1, 2, 3, 4, 5, 6, 7]

❷ 当有引用类型值时:

let arr = [1, 2, 3, 4, 5, 6, [1, 2, 3]];
let arr1 = [...arr];
arr1.push(7);
arr1[arr1.length - 2][0] = 100;
console.log(arr); //[1, 2, 3, 4, 5, 6,[100, 2, 3]]
console.log(arr1); //[1, 2, 3, 4, 5, 6, [100, 2, 3],7]

3. 深拷贝: 完全拷贝一个新对象,修改时原对象不再受到任何影响,这是我们最想看到的。

  • JSON方法实现,性能最好
let deepClone = function (obj) {
  let _tmp = JSON.stringify(obj);  //将对象转换为json字符串形式
  let result = JSON.parse(_tmp);  //将转换而来的字符串转换为原生js对象
  return result;
};

let obj = {
  xiaoming: {
    age: 20,
    class: 1502
  },
  xiaofang: {
    age: 21,
    class: 1501
  }
};

let result = deepClone(obj);
result.xiaofang.gender = "male";
console.log(result);  //{ xiaoming: { age: 20, class: 1502 }, xiaofang: { age: 21, class: 1501, gender: 'male' } }
console.log(obj);  // { xiaoming: { age: 20, class: 1502 }, xiaofang: { age: 21, class: 1501 } }

但是也有局限性:
对象的属性值是函数时,无法拷贝;
原型链上的属性无法拷贝;
不能正确的处理 Date 类型的数据;
不能处理 RegExp
会忽略 symbol
会忽略 undefined

  • for…in实现遍历和复制,使用for...in会遍历对象的所有可枚举属性,包括原型。

基本实现就是利用递归来实现深拷贝,如果对象属性的值是引用类型(Array,Object),那么对该属进行遍历深拷贝,直到遍历到属性的值是基本类型为止。

function deepClone(obj){
  //因为null是基础类型,但是用typeof判断时,显示为object。原理是因为是使用二进制判断,前三位为0则为object,而null,所有位数均为0,故显示为object
  if(obj === null) return null;
  if(typeof obj !== 'object') return obj;
  if(obj.constructor === Date) return new Date(obj);
  if(obj.constructor === RegExp) return new RegExp(obj);
  var newObj = new obj.constructor();  //保持继承链
  for (var key in obj) {
    if (obj.hasOwnProperty(key)) {   //不遍历其原型链上的属性
      var val = obj[key];
      newObj[key] = typeof val === 'object' ? arguments.callee(val) : val; // 使用arguments.callee解除与函数名的耦合,判断需不需要递归遍历拷贝
    }
  }
  return newObj;
}

let obj = {
  xiaoming: {
    age: 20,
    class: 1502
  },
  xiaofang: {
    age: 21,
    class: 1501
  }
};

let result = deepClone(obj);
result.xiaofang.gender = "male";
console.log(result);   //{ xiaoming: { age: 20, class: 1502 }, xiaofang: { age: 21, class: 1501, gender: 'male' } }
console.log(obj);  //{ xiaoming: { age: 20, class: 1502 }, xiaofang: { age: 21, class: 1501 } }
  • 利用数组的Array.prototype.forEach进行拷贝
let deepClone = function (obj) {
  let copy = Object.create(Object.getPrototypeOf(obj)); //Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
  let propNames = Object.getOwnPropertyNames(obj);  //Object.getOwnPropertyNames()方法返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组。
  propNames.forEach(function (items) {
    let item = Object.getOwnPropertyDescriptor(obj, items);  //Object.getOwnPropertyDescriptor() 方法返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)
    Object.defineProperty(copy, items, item);
  });
  return copy;
};

let obj = {
  xiaoming: {
    age: 20,
    class: 1502
  },
  xiaofang: {
    age: 21,
    class: 1501
  }
};

let result = deepClone(obj);
result.xiaofang.gender = "male";
console.log(result);
console.log(obj);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值