作为一个刚学前端不久的小菜菜,最近用了很多次loadsh的深拷贝,就想着自己实现一下深拷贝,了解一下基础的实现。
就先从值类型和引用类型开始
1. 数据类型
1.1 值数据类型
保存在栈内存中的简单的数据类型,包含Number、String、Boolean、Null、Undefined。
1.2 引用数据类型
保存在堆内存中的对象,所以引用类型值保存的是一个指针,这个指针指向存在堆中的一个对象。
2. 浅拷贝
被拷贝引用类型,只拷贝了一层,开辟了新的内存空间把相应的值保存复制到了新的内存空间里,对于第一层中的引用类型,复制了他们的内存地址,而不是值。
2.1 对于数组
2.1.1 扩展运算符
const arr = [1,2,3,{a: 1}];
const arr1 = [...arr];
arr1[3].a = 2; // 此时arr[3].a的值也被改变
arr[0] = 2; // 此时arr1[0]的值不受影响
2.1.2 slice
const arr2 = arr.slice(0);
arr2[3].a = 3; // 此时arr[3].a的值也被改变
2.1.3 concat
const arr3 = [].concat(arr);
arr3[3].a = 4; // 此时arr[3].a的值也被改变
2.1.4 new Set
const arr4 = [...new Set(arr)];
arr4[3].a = 5; // 此时arr[3].a的值也被改变
2.2 对于对象
2.2.1 扩展运算符
const obj = { a: [1, { b: 2 }], c: { d: { e: 1 } } }
const obj1 = {...obj};
obj1.a[1].b = 3; // 此时obj.a[1].b的值也会改变
obj1.a = 2; // 此时obj.a的值不受影响
2.2.2 Object.assign
const obj2 = Object.assign({}, obj);
obj2.c.d = 5; // 此时obj.c.d的值也会改变
3. 深拷贝
被拷贝引用类型和被赋值变量所在的内存空间是两个不同的地址,修改被拷贝对象所有属性的值的时候不会影响被赋值对象所在空间
3.1 JSON方式实现深拷贝
JSON格式可以支持的数据类型
- 简单值:
字符串、数值、布尔值和null - 对象类型,
键值对 - 数组类型,
数组的值可以是任意值
在JavaScript中JSON格式支持的类型有哪些?那么每一个类型被解析前后的变化是如何的?验证一下
const s = 'string';
const num = 123;
const num1 = NaN;
const num2 = Infinity;
const n = null;
const un = undefined;
const fn = function () { };
const o = { a: 1 };
const map = new Map([['123', 123]]);
const list = [1, 2, 3];
const set = new Set([1, 2, 3]);
const sym = Symbol();
const reg = new RegExp('ab+c', 'i');
const date = new Date();
const typeData = {
s,
// num,
// num1,
num2,
n,
un,
fn,
o,
map,
list,
set,
sym,
reg,
date
}
let jsonType = getTypeMap(typeData);
function getTypeMap(typeData) {
let result = new Map();
Object.keys(typeData).forEach(item => {
const key = Object.prototype.toString.call(typeData[item]);
const value = JSON.parse(JSON.stringify({ [key]: typeData[item] }));
result.set(key, value);
});
return result
}
console.log(jsonType);
如果map中的key对应的value属性或者值丢失,那么就说明JSON不能表示该类型。可以自己执行一下看一下结果,是不是对于数字类型的那块有疑问,为什么值是null,可以自己打个断点走调试看看,因为num、num1、num2这三个类型相同他们的Map中的键相同,值发生了替换
| JS数据类型 | JSON格式是否支持 | 解析结果 |
|---|---|---|
| Number | 是 | 数字 (NaN和Infinity会被转化为null) |
| String | 是 | 字符串 |
| null | 是 | null |
| undefined | 否 | 属性消失 |
| Function | 否 | 属性消失 |
| Object | 是 | 对象 |
| Array | 是 | 数组 |
| Date | 是 | 字符串 |
| Map | 否 | 属性存在,值为{} |
| Set | 否 | 属性存在,值为{} |
| Symbol | 否 | 属性消失 |
| RegExp | 否 | 属性存在,值为{} |
【JSON实现深拷贝】
了解一下JSON.parse() ,是用来解析JSON字符串,参数是一个JSON字符串格式的数据,否则会报错,那么下面的对于格式判断怎么加,有想法可以在评论下告诉我,我白嫖一下。
JSON字符串格式
- “名称/值”对的集合
- 值的有序列表
function deepClone(arg) {
if (arg === null || typeof arg !== 'object' || isDateType(arg)) throw new TypeError('形参必须是一个对象类型或者数组类型的值');
return JSON.parse(JSON.stringify(arg))
}
function isDateType(arg) {
return Object.prototype.toString.call(arg) === '[object Date]'
}
用上面的typeData的数据测试一下
const result = deepClone(typeData);
console.log(result);
/*
和上面表中的undefined、函数、Symbol属性消失,NaN和Infinity会被转化为null,Map、Set、RegExp值会消失,值为{}
{
s: 'string',
num: 123,
num1: null,
num2: null,
n: null,
o: { a: 1 },
map: {},
list: [ 1, 2, 3 ],
set: {},
reg: {},
date: '2023-03-15T06:23:29.893Z'
}
*/
3.2 递归实现深拷贝(对象或数组)
通过遍历每一层的数据,如果是基础类型就直接赋值,如果是对象类型或者数组类型,就接着递归遍历执行上面的操作。下面自己实现一遍。
function deepClone(arg) {
const isArray = Array.isArray(arg); // 判断传入参数是不是一个数组
const result = isArray ? [] : {}; // 设置容器
// 遍历对象,如果是数组就是当前参数,如果不是数组就是keys
const rangeData = isArray ? arg : Object.keys(arg);
rangeData.forEach((item, index) => {
const flag = isArray ? index : item;
// 如果rangeData是数组,则当前元素是item,如果rangeData是对象,则当前元素是arg[item]
const cloneTarget = isArray ? item : arg[item];
if(isLikeObject(flag, arg)) {
result[flag] = deepClone(cloneTarget);
}else {
result[flag] = cloneTarget;
}
})
return result
}
function isLikeObject(flag, self) {
return typeof self[flag] === 'object' && self[flag] !== null
}
测试一下,确实完成了深层拷贝,但是还是有一些问题
const result = deepClone(data);
console.log(result);
/*
相较于之前JSON实现深拷贝,现在实现了对于NaN、Infinity、undefined的深拷贝,Set、Map、正则、Date会出现数据丢失的情况,对于函数的拷贝引用关系
{
s: 'string',
num: 123,
num1: NaN,
num2: Infinity,
n: null,
un: undefined,
fn: [Function: fn],
o: { a: 1 },
map: {},
list: [ 1, 2, 3 ],
set: {},
sym: Symbol(),
reg: {},
date: {}
}
*/
修改一下
function deepClone(arg) {
if(arg instanceof Date) return new Date(arg);
if(arg instanceof RegExp) return new RegExp(arg);
const isArray = Array.isArray(arg); // 判断传入参数是不是一个数组
const result = isArray ? [] : {}; // 设置容器
// 遍历对象,如果是数组就是当前参数,如果不是数组就是keys
const rangeData = isArray ? arg : Object.keys(arg);
rangeData.forEach((item, index) => {
const flag = isArray ? index : item;
// 如果rangeData是数组,则当前元素是item,如果rangeData是对象,则当前元素是arg[item]
const cloneTarget = isArray ? item : arg[item];
if(isLikeObject(flag, arg)) {
result[flag] = deepClone(cloneTarget);
}else {
result[flag] = cloneTarget;
}
})
return result
}
function isLikeObject(flag, self) {
return typeof self[flag] === 'object' && self[flag] !== null
}
3.3 循环引用问题
关于循环引用的问题,下面是一个循环引用的例子,分别用JSON和递归拷贝一下循环引用的对象,会发生什么?
const a = {};
const b = {c: { d: { e: a}}};
a.f = b;
试着用JSON深拷贝拷贝一下,报错类型错误,转换循环结构为JSON
// TypeError: Converting circular structure to JSON
用递归深拷贝拷贝一下,范围错误:已超过最大调用堆栈大小
// RangeError: Maximum call stack size exceeded
解决一下循环引用的问题:
通过WeakMap的形式,将复制过的变量名存下来,等下次出现直接赋值,现在有三个问题
- 在哪创建WeakMap
- 设置什么值
- 在哪设置WeakMap元素的值
在形参里初始化WeakMap,在开始保存当前拷贝对象,以当前拷贝对象作为键值,在拷贝对象的时候判断当WeakMap中是否有这个键,有的话,就直接赋值
4. 修改代码,最终实现
function deepClone(arg, hash = new WeakMap()) {
if(arg instanceof Date) return new Date(arg);
if(arg instanceof RegExp) return new RegExp(arg);
const isArray = Array.isArray(arg); // 判断传入参数是不是一个数组
hash.set(arg, isArray ? [...arg] : {...arg});
const result = isArray ? [] : {}; // 设置容器
// 遍历对象,如果是数组就是当前参数,如果不是数组就是keys
const rangeData = isArray ? arg : Object.keys(arg);
rangeData.forEach((item, index) => {
const flag = isArray ? index : item;
// 如果rangeData是数组,则当前元素是item,如果rangeData是对象,则当前元素是arg[item]
const cloneTarget = isArray ? item : arg[item];
if(isLikeObject(flag, arg)) {
if(hash.has(cloneTarget)) {
result[flag] = hash.get(cloneTarget);
}else {
result[flag] = deepClone(cloneTarget, hash);
}
}else {
result[flag] = cloneTarget;
}
})
return result
}
function isLikeObject(flag, self) {
return typeof self[flag] === 'object' && self[flag] !== null
}