深拷贝和浅拷贝概述
为什么会有深拷贝和浅拷贝?
- JS的数据类型分为基本类型和接应类型。
- 基本类型:直接存储在栈(stack)中的数据。
- 接引类型:存储的是该对象在栈中的引用(即指针),真实的数据存放在堆内存里。
- 深拷贝和浅拷贝针对的是接引类型。理论上基本类型赋值都属于深拷贝,这是因为基本类型的不可变性。
深拷贝
- 复制接应类型的真实数据。不直接复制引用地址(即指针)。
浅拷贝
- 只复制引用地址(即指针)。不改变接应类型的真实数据
实现一层深拷贝的一些方法
for in 循环复制对象
const obj = { a: 1 }; // 需要被拷贝的对象
const obj2 = {}; // 拷贝的新对象
// 循环遍历
for (const key in obj) {
Object.prototype.hasOwnProperty(obj, key) && (obj2[key] = obj[key]);
}
obj2.a = 2; // 修改新的对象验证
console.log(obj.a); // 1
console.log(obj2.a); // 2
Object.assgin
- 这是 ES6 新增的静态方法,可用于深拷贝对象。
- 用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
const obj = { a: 1 }; // 需要被拷贝的对象
const obj2 = {}; // 拷贝的新对象
Object.assign(obj2, obj); // 深拷贝
obj2.a = 2; // 修改新的对象验证
console.log(obj.a); // 1
console.log(obj2.a); // 2
扩展运算符
const obj = { a: 1 }; // 需要被拷贝的对象
const obj2 = { ...obj }; // 拷贝的新对象
obj2.a = 2; // 修改新的对象验证
console.log(obj.a); // 1
console.log(obj2.a); // 2
Object.entries + forEach
- Object.entries是 ES8 新增的静态方法。
- 返回一个给定对象自身可枚举属性的键值对数组,其排列与使用 for…in 循环遍历该对象时返回的顺序一致(区别在于 for-in 循环还会枚举原型链中的属性)。
- forEach 为 ES5 的迭代遍历API。
const obj = { a: 1 }; // 需要被拷贝的对象
const obj2 = {}; // 拷贝的新对象
// 循环遍历
Object.entries(obj).forEach(elem => {
obj2[elem[0]] = elem[1];
});
obj2.a = 2; // 修改新的对象验证
console.log(obj.a); // 1
console.log(obj2.a); // 2
JSON.parse 和 JSON.stringify
- 该方法可用于多层深拷贝。
- 原理:通过 JSON.stringify 把需要深拷贝的接引类型转成 JSON 字符串,而字符串是基本类型,所以通过 JSON.parse 把 JSON 字符串解析成接引类型其通过了字符串的不可变性实现深拷贝的目的。
- 注意:此方法虽然省事但是存在一些缺陷。
- 如果 obj 里面有时间对象,则 JSON.stringify 后再 JSON.parse 的结果,时间将只是字符串的形式。而不是时间对象。
- 如果 obj 里有 RegExp、Error 对象,则序列化的结果将只得到空对象。
- 如果 obj 里有函数,undefined,则序列化的结果会把函数或 undefined 丢失。
- 如果 obj 里有 NaN、Infinity 和 -Infinity,则序列化的结果会变成 null。
- JSON.stringify() 只能序列化对象的可枚举的自有属性。
- 如果obj中的对象是有构造函数生成的,则使用 JSON.parse(JSON.stringify(obj)) 深拷贝后,会丢弃对象的 constructor。
- 如果对象中存在循环引用的情况也无法正确实现深拷贝。
const obj = { a: 1 }; // 需要被拷贝的对象
const obj2 = JSON.parse(JSON.stringify(obj)); // 拷贝的新对象
obj2.a = 2; // 修改新的对象验证
console.log(obj.a); // 1
console.log(obj2.a); // 2
兼容性
- 关于其中的兼容性可以考虑使用 babel 来转译成 ES5。
封装方法深拷贝
支持类型
- 暂时只支持 String,Number,Boolean,null,undefined,Object,Array,Date,RegExp,Error 类型的深拷贝。
代码
/**
* 变量类型判断
* @param {String} type - 需要判断的类型
* @param {Any} value - 需要判断的值
* @returns {Boolean} - 是否该类型
*/
const IsType = (type, value) => Object.prototype.toString.call(value) === `[object ${type}]`;
/**
* 深拷贝变量-递归算法(recursive algorithm)
* 支持 String,Number,Boolean,null,undefined,Object,Array,Date,RegExp,Error 类型
* @param {Any} arg - 需要深拷贝的变量
* @returns {Any} - 拷贝完成的值
*/
const DeepCopyRA = arg => {
const newValue = IsType("Object", arg) // 判断是否是对象
? {}
: IsType("Array", arg) // 判断是否是数组
? []
: IsType("Date", arg) // 判断是否是日期对象
? new arg.constructor(+arg)
: IsType("RegExp", arg) || IsType("Error", arg) // 判断是否是正则对象或错误对象
? new arg.constructor(arg)
: arg;
// 判断是否是数组或对象
if (IsType("Object", arg) || IsType("Array", arg)) {
// 循环遍历
for (const key in arg) {
// 防止原型链的值
Object.prototype.hasOwnProperty.call(arg, key) && (newValue[key] = DeepCopyRA(arg[key]));
}
}
return newValue;
};
const obj = { a: { a: 1 }, b: [1, 2, 3], c: new Date(), d: /^\d{6}$/, e: 5, f: undefined, g: null }; // 需要被拷贝的对象
const obj2 = DeepCopyRA(obj); // 拷贝的新对象
obj2.a.a = 2; // 修改新的对象验证
obj2.b[0] = 2; // 修改新的对象验证
console.log(obj); // { a: { a: 1 }, b: (3)[(1, 2, 3)], c: "Thu Jun 18 2020 11:29:02 GMT+0800 (中国标准时间) {}", d: /^\d{6}$/, e: 5, f: undefined, g: null };
console.log(obj2); // { a: { a: 2 }, b: (3)[(2, 2, 3)], c: "Thu Jun 18 2020 11:29:02 GMT+0800 (中国标准时间) {}", d: /^\d{6}$/, e: 5, f: undefined, g: null };