JavaScript深拷贝与浅拷贝

1 什么是深/浅拷贝

浅拷贝👇

浅拷贝就是创建一个新对象,该对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象

深拷贝👇

深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象


例如,当一个对象people中包含了另一个对象children,对people进行浅拷贝得到了people2,如下所示:

let people = { // 原对象
  name: "张三",
  children: {
    name: "李四",
    age: 2,
  },
};
let people2 = Object.assign({}, people); // 对people进行浅拷贝

如果修改people2childrennameage,根据浅拷贝特性,我们可以推断出peoplechildren也会被修改,因为people2children是引用类型,拷贝的是内存地址,修改people2会影响people,如下所示:

people2.children.name = "王五";
people2.children.age = 5;

people // { name: '张三', children: { name: '王五', age: 5 } }
people2 // { name: '张三', children: { name: '王五', age: 5 } }

如果修改people2name,根据浅拷贝特性,我们可以推断出peoplename不会收到影响,因为people2name是基本类型,拷贝的是peoplename的值,如下所示:

people2.name = "橘猫吃不胖";

people // { name: '张三', children: { name: '李四', age: 2 } }
people2 // { name: '橘猫吃不胖', children: { name: '李四', age: 2 } }

这时出现了一个特殊情况:如果浅拷贝的对象只有一层的时候,其实就是深拷贝了,示例代码如下:

let people = {
  name: "橘猫吃不胖",
  age: 2,
};
let people2 = Object.assign({}, people); // 浅拷贝

people2.name = "张三";
people2.age = 4;

people // { name: '橘猫吃不胖', age: 2 }
people2 // { name: '张三', age: 4 }

如果对people进行深拷贝得到people3,如下所示:

let people = { // 原对象
  name: "张三",
  children: {
    name: "李四",
    age: 2,
  },
};
let people3 = JSON.parse(JSON.stringify(people)); // 深拷贝

无论修改people3name还是children,根据深拷贝特性,我们可以推断都不会影响到people,因为深拷贝后,peoplepeople3使用的是不同的内存地址,如下所示:

people3.name = "赵六";
people3.children.name = "赵六的儿子";
people3.children.age = 8;

people // { name: '张三', children: { name: '李四', age: 2 } }
people3 // { name: '赵六', children: { name: '赵六的儿子', age: 8 } }

2 浅拷贝与赋值的区别

根据上面的描述,我们可能会想,浅拷贝和赋值好像没有什么区别。

其实不然,当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是说两个对象指向了同一个存储空间,无论哪个对象发生改变,都会影响另一个对象。

例如,还是people对象,将它赋值给people4,如下所示:

let people = {
  name: "张三",
  children: {
    name: "李四",
    age: 2,
  },
};
let people4 = people; // 直接赋值

当我们修改people4name或者children时,根据赋值的特点,我们可以推断peoplename或者children都会受到影响:

people4.name = "people4";
people4.children.name = "people4的孩子";
people4.children.age = 1;

people // { name: 'people4', children: { name: 'people4的孩子', age: 1 } }
people4 // { name: 'people4', children: { name: 'people4的孩子', age: 1 } }

与在前面进行浅拷贝得到的people2相比,people2修改name之后并没有影响people的值,因为浅拷贝对基本类型拷贝了值,这就是两者的区别。

3 浅拷贝的实现

3.1 Object.assign()

Object.assign()方法在开始就已经说明过了,它本身用于将所有可枚举属性的值从一个或多个源对象分配到目标对象,最后返回目标对象。基本语法如下:

Object.assign(target, ...sources) // target:目标对象;sources:源对象。

如果目标对象target中的属性与源对象sources具有相同的键,则相同的属性会被源对象中的属性覆盖。后面的源对象的属性将类似地覆盖前面的源对象的属性。如下所示:

// 目标对象和源对象具有相同的属性b
const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };

// 浅拷贝,目标对象的b属性值会被源对象覆盖
const returnedTarget = Object.assign(target, source);

target // { a: 1, b: 4, c: 5 }
returnedTarget // { a: 1, b: 4, c: 5 }

使用Object.assign()进行浅拷贝,示例如下:

let obj1 = {
  a: 1,
  b: {
    c: 1,
    d: 1,
  },
};
let obj2 = Object.assign({}, obj1); // 进行浅拷贝

obj2.a = 2; // 修改拷贝后的对象
obj2.b.c = 2;
obj2.b.d = 2;

obj1 // { a: 1, b: { c: 2, d: 2 } }
obj2 // { a: 2, b: { c: 2, d: 2 } }

3.2 Array.prototype.concat()

concat()方法用于合并两个或多个数组。此方法不会更改现有数组,返回了一个新数组。基本语法如下:

var new_array = old_array.concat(value1[, value2[, ...[, valueN]]])
// valueN可选,数组和/或值,将被合并到一个新的数组中。
// 如果省略了所有valueN参数,则concat会返回调用此方法的现存数组的一个浅拷贝。

使用Array.prototype.concat()进行浅拷贝,示例代码如下:

let arr1 = [1, 2, { name: "橘猫吃不胖" }];
let arr2 = arr1.concat(); // 进行浅拷贝

// 修改arr2
arr2[1] = 99999999;
arr2[2].name = "张三";

arr1 // [ 1, 2, { name: '张三' } ]
arr2 // [ 1, 99999999, { name: '张三' } ]

3.3 Array.prototype.slice()

slice()方法返回一个新的数组对象,这一对象是一个由beginend决定的原数组的浅拷贝(包括begin,不包括end),原始数组不会被改变。

使用Array.prototype.slice()进行浅拷贝,示例代码如下:

let arr1 = [1, 2, { name: "橘猫吃不胖" }];
let arr2 = arr1.slice(); // 进行浅拷贝

// 修改arr2
arr2[1] = 99999999;
arr2[2].name = "张三";

arr1 // [ 1, 2, { name: '张三' } ]
arr2 // [ 1, 99999999, { name: '张三' } ]

3.4 …扩展运算符

使用...扩展运算符可以实现浅拷贝,示例代码如下:

let obj1 = {
  a: 1,
  b: {
    c: 1,
    d: 1,
  },
};
let obj2 = { ...obj1 }; // 进行浅拷贝

obj2.a = 2; // 修改拷贝后的对象
obj2.b.c = 2;
obj2.b.d = 2;

obj1; // { a: 1, b: { c: 2, d: 2 } }
obj2; // { a: 2, b: { c: 2, d: 2 } }

4 深拷贝的实现

4.1 JSON.parse(JSON.stringify())

JSON是一种语法,用来序列化对象、数组、数值、字符串、布尔值和 null 。

JSON对象包含两个方法:

  • 解析JSON的parse()方法JSON.parse()
  • 将对象/值转换为JSON字符串的stringify()方法,JSON.stringify()

JSON.parse()方法用来解析JSON字符串,构造由字符串描述的JavaScript值或对象。提供可选的reviver函数用以在返回之前对所得到的对象执行变换。基本语法如下:

JSON.parse(text[, reviver])
// text:要被解析成 JavaScript 值的字符串
// reviver,可选,转换器, 如果传入该参数(函数),可以用来修改解析生成的原始值,调用时机在 parse 函数返回之前。

JSON.stringify()方法将一个JavaScript对象或值转换为JSON字符串,如果指定了一个replacer函数,则可以选择性地替换值,或者指定的replacer是数组,则可选择性地仅包含数组指定的属性。基本语法如下:

JSON.stringify(value[, replacer [, space]])
// value:将要序列化成 一个 JSON 字符串的值。
// replacer,可选,如果该参数是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理;如果该参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中;如果该参数为 null 或者未提供,则对象所有的属性都会被序列化。
// space,可选,指定缩进用的空白字符串,用于美化输出(pretty-print);如果参数是个数字,它代表有多少的空格;上限为10。该值若小于1,则意味着没有空格;如果该参数为字符串(当字符串长度超过10个字母,取其前10个字母),该字符串将被作为空格;如果该参数没有提供(或者为 null),将没有空格。

使用JSON.parse(JSON.stringify())进行深拷贝,示例代码如下:

let obj1 = {
  a: 1,
  b: {
    c: 1,
    d: 1,
  },
};
let obj2 = JSON.parse(JSON.stringify(obj1)); // 进行深拷贝

obj2.a = 2; // 修改拷贝后的对象
obj2.b.c = 2;
obj2.b.d = 2;

obj1; // { a: 1, b: { c: 1, d: 1 } }
obj2; // { a: 2, b: { c: 2, d: 2 } }

JSON.stringify将对象转成JSON字符串,再用JSON.parse()把字符串解析成对象,这样新的对象产生了,实现深拷贝。这种方法虽然可以实现数组或对象深拷贝,但有一些缺点:

  • 无法拷贝循环引用
let obj1 = { a: 2 };
obj1.target = obj1; // 循环引用
let obj2 = JSON.parse(JSON.stringify(obj1));

obj2 // TypeError: Converting circular structure to JSON
  • 无法拷贝函数
let arr1 = [1, 2, { name: "橘猫吃不胖" }, function () {}];
let arr2 = JSON.parse(JSON.stringify(arr1)); // 进行深拷贝

arr1 // [ 1, 2, { name: '橘猫吃不胖' }, [Function (anonymous)] ]
arr2 // [ 1, 2, { name: '橘猫吃不胖' }, null ]

4.2 函数库lodash

Lodash是一个JavaScript库,提供了多个实用程序功能,而Lodash库中最常用的功能之一是cloneDeep()方法。此方法有助于深度克隆对象,还可以打破JSON.parse(JSON.stringify())方法的局限性,即拷贝不可序列化的属性。

使用cloneDeep()进行深拷贝,示例代码如下:

const lodash = require("lodash");
let arr1 = [1, 2, { name: "橘猫吃不胖" }, function () {}];
let arr2 = lodash.cloneDeep(arr1); // 进行深拷贝

arr2[0] = 99999;
arr2[3].name = "张三";

arr1; // [ 1, 2, { name: '橘猫吃不胖' }, [Function (anonymous)] ]
arr2; // [ 99999, 2, { name: '橘猫吃不胖' }, [Function (anonymous)] ]

5 实现一个深拷贝

根据深拷贝的特点,我们首先可以实现一个简易版本:

  • 如果是基本数据类型,直接返回
  • 如果是引用数据类型,就要递归调用深拷贝函数,直到拷贝到最后一层

基础版本如下:

function deepClone(origin) {
  // 如果原对象是基本数据类型,直接返回即可
  if (!origin || typeof origin !== "object") return origin;
  // 根据原对象是数组还是对象,创建一个新的数组或者对象
  let target = Array.isArray(origin) ? [] : {};
  // 遍历当前对象,放在target中,如果对象中包含下一层,通过递归来遍历
  for (let i in origin) {
    target[i] = deepClone(origin[i]);
  }
  // 返回深拷贝对象
  return target;
}

这个函数目前还存在一些问题,如果对象存在循环引用,会进入死循环导致栈内存溢出:

let obj1 = { a: 1 };
obj1.target = obj1; // 自己指向自己

deepClone(obj1); // RangeError: Maximum call stack size exceeded

想要解决循环引用,我们可以额外开辟一个可以存储key: value形式的存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝。这里我们使用Map来实现。

  • 检查map中有无克隆过的对象
  • 如果有,直接返回map中存储的值,如果没有,在map中存储
function deepClone(origin, map = new Map()) {
  // 如果原对象是基本数据类型,直接返回即可
  if (!origin || typeof origin !== "object") return origin;
  // 根据原对象是数组还是对象,创建一个新的数组或者对象
  let target = Array.isArray(origin) ? [] : {};
  // 如果Map中已经存储了当前的对象,直接返回Map存储的结果即可,否则存放origin
  if (map.get(origin)) return map.get(origin);
  map.set(origin, target);
  // 遍历当前对象,放在target中,如果对象中包含下一层,通过递归来遍历
  for (let i in origin) {
    target[i] = deepClone(origin[i], map);
  }
  // 返回深拷贝对象
  return target;
}

再执行一下上面的例子,可以看到成功拷贝了:

let obj1 = { a: 1 };
obj1.target = obj1;

console.log(deepClone(obj1)); // <ref *1> { a: 1, target: [Circular *1] }
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值