先说说深拷贝和浅拷贝
- 浅拷贝
所谓浅拷贝,就是只复制最外一层,里面的都还是相同引用
// 浅拷贝
const a = { name: 'xiaoming', age: 23 }
const b = {}
for (let key in a){
b[key] = a[key]
}
console.log(b) // { name: 'xiaoming', age: 23 }
console.log(b === a) // false
console.log(b.name === a.name) // true
- 深拷贝
深拷贝,是将一个对象拷贝到另一个新变量,这个新变量指向的是一块新的堆内存地址
// 深拷贝
function cloneDeep(target) {
// ...dosomething 实现深拷贝
}
const a = { name: 'xiaoming', age: 23 }
const b = cloneDeep(a)
console.log(b) // { name: 'xiaoming', age: 23}
console.log(b === a) // false
console.log(b.name === a.name) // false
- 深拷贝实现代码
常用版本
function cloneDeep(target) {
return JSON.parse(JSON.stringify(target))
}
const a = { name: 'xiaoming', age: 23 }
const b = cloneDeep(a)
console.log(b) // { name: 'xiaoming', age: 23 }
console.log(b === a) // false
虽然大多数时候这么使用是没问题的,但这种方式在工作中还是有很多问题
1、对象中有字段值为undefined
,转换后则会直接字段消失
2、对象如果有字段值为RegExp
对象,转换后则字段值会变成{}
3、对象如果有字段值为NaN、+-Infinity
,转换后则字段值变成null
4、对象如果有环引用
,转换直接报错
升级一下
function cloneDeep(target) {
const temp = {}
for (const key in target) {
temp[key] = target[key]
}
return temp
}
const a = { name: 'xiaoming', age: 23 }
const b = cloneDeep(a)
console.log(b) // { name: 'xiaoming', age: 23 }
console.log(b === a) // false
看起来不错了,但是遇到层数比较深的还是不可以,在优化一下,递归一下。
function cloneDeep(target) {
// 基本数据类型直接返回
if (typeof target !== 'object') {
return target
}
// 引用数据类型特殊处理
const temp = {}
for (const key in target) {
// 递归
temp[key] = cloneDeep(target[key])
}
return temp
}
const a = {
name: 'xiaoming',
age: 23,
hobbies: { sports: 'swim', game: 'lol' }
}
const b = cloneDeep(a)
console.log(b === a) // false
但是现在只可以是拷贝对象,数组还不行,在优化一下。
function cloneDeep(target) {
// 基本数据类型直接返回
if (typeof target !== 'object') {
return target
}
// 引用数据类型特殊处理
// 判断数组还是对象
const temp = Array.isArray(target) ? [] : {}
for (const key in target) {
// 递归
temp[key] = cloneDeep(target[key])
}
return temp
}
const a = {
name: 'xiaoming',
age: 23,
hobbies: { sports: 'swim', game: 'lol' },
works: ['2020', '2021']
}
const b = cloneDeep(a)
console.log(b === a) // false
但是并没有解决一个重要的问题,环引用的问题。
- 什么是循环引用
当对象 1 中的某个属性指向对象 2,对象 2 中的某个属性指向对象 1 就会出现循环引用,(当然不止这一种情况,不过原理是一样的)下面通过代码来说明一下。
let obj1 = {};
let obj2 = {
b: obj1
};
obj1.a = obj2;
JSON.tostringify 无法将一个无限引用的对象序列化为 JOSN 字符串。
下面是JSON.stringify() 将值转换为相应的JSON格式的解释:
- 转换值如果有 toJSON() 方法,该方法定义什么值将被序列化。
- 非数组对象的属性不能保证以特定的顺序出现在序列化后的字符串中。
- 布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值。
- undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。函数、undefined 被单独转换时,会返回 undefined,如JSON.stringify(function(){}) or JSON.stringify(undefined).
- 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
- 所有以 symbol 为属性键的属性都会被完全忽略掉,即便 replacer 参数中强制指定包含了它们。
- Date 日期调用了 toJSON() 将其转换为了 string 字符串(同Date.toISOString()),因此会被当做字符串处理。
- NaN 和 Infinity 格式的数值及 null 都会被当做 null。
- 其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性。
当然解决上述的方法也可以这么写
let obj1 = {
};
let obj2 = {
b: obj1
};
obj1.a = obj2;
let c = JSON.decycle(obj1);
JSON.stringify(c)
结合之上的问题,在升级一下深拷贝
function cloneDeep(target, map = new Map()) {
// 基本数据类型直接返回
if (typeof target !== 'object') {
return target
}
// 引用数据类型特殊处理
// 判断数组还是对象
const temp = Array.isArray(target) ? [] : {}
if (map.get(target)) {
// 已存在则直接返回
return map.get(target)
}
// 不存在则第一次设置
map.set(target, temp)
for (const key in target) {
// 递归
temp[key] = cloneDeep(target[key], map)
}
return temp
}
const a = {
name: 'xiaoming',
age: 23,
hobbies: { sports: 'swim', game: 'lol' },
works: ['2020', '2021']
}
a.key = a // 环引用
const b = cloneDeep(a)
console.log(b === a) // false
看起来可以了,但是并不能实现所有的需求,只能实现一些比较常见的数组、对象等等 。
但其实,引用数据类型可不止只有数组和对象,我们还得解决以下的引用类型的拷贝问题,那怎么判断每个引用数据类型的各自类型呢?可以使用Object.prototype.toString.call()
这个会返回所有的数据类型如下;
类型 | ||
Map | Object.prototype.toString.call(new Map()) | [object Map] |
Set | Object.prototype.toString.call(new Set()) | [object Set] |
Array | Object.prototype.toString.call([]) | [object Array] |
Object | Object.prototype.toString.call({}) | [object Object] |
Symbol | Object.prototype.toString.call(Symbol()) | [object Symbol] |
RegExp | Object.prototype.toString.call(new RegExp()) | [object RegExp] |
Function | Object.prototype.toString.call(function() {}) | [object Function] |
把上树类型分成两类,第一类为可遍历,第二类为不可遍历。
// 可遍历的类型
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
// 不可遍历类型
const symbolTag = '[object Symbol]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';
// 将可遍历类型存在一个数组里
const canForArr = ['[object Map]', '[object Set]','[object Array]', '[object Object]']
// 将不可遍历类型存在一个数组
const noForArr = ['[object Symbol]', '[object RegExp]', '[object Function]']
// 判断类型的函数
function checkType(target) {
return Object.prototype.toString.call(target)
}
// 判断引用类型的temp
function checkTemp(target) {
const c = target.constructor
return new c()
}
判断两种的方法总结一下
// 拷贝Function的方法
function cloneFunction(func) {
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
if (func.prototype) {
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if (body) {
if (param) {
const paramArr = param[0].split(',');
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
} else {
return null;
}
} else {
return eval(funcString);
}
}
// 拷贝Symbol的方法
function cloneSymbol(targe) {
return Object(Symbol.prototype.valueOf.call(targe));
}
// 拷贝RegExp的方法
function cloneReg(targe) {
const reFlags = /\w*$/;
const result = new targe.constructor(targe.source, reFlags.exec(targe));
result.lastIndex = targe.lastIndex;
return result;
}
结合一下 最终版本
function cloneDeep(target, map = new Map()) {
// 获取类型
const type = checkType(target)
// 基本数据类型直接返回
if (!canForArr.concat(noForArr).includes(type)) return target
// 判断Function,RegExp,Symbol
if (type === funcTag) return cloneFunction(target)
if (type === regexpTag) return cloneReg(target)
if (type === symbolTag) return cloneSymbol(target)
// 引用数据类型特殊处理
const temp = checkTemp(target)
if (map.get(target)) {
// 已存在则直接返回
return map.get(target)
}
// 不存在则第一次设置
map.set(target, temp)
// 处理Map类型
if (type === mapTag) {
target.forEach((value, key) => {
temp.set(key, cloneDeep(value, map))
})
return temp
}
// 处理Set类型
if (type === setTag) {
target.forEach(value => {
temp.add(cloneDeep(value, map))
})
return temp
}
// 处理数据和对象
for (const key in target) {
// 递归
temp[key] = cloneDeep(target[key], map)
}
return temp
}
const a = {
name: 'xiaoming',
age: 23,
hobbies: { sports: 'swim', game: 'lol' },
works: ['2020', '2021']
map: new Map([['aaa', 0001], ['bbb', 0002]]),
set: new Set([1, 2, 3]),
func: (do) => `${do}!`,
sym: Symbol(1111),
reg: new RegExp(/oooooooooo/g),
}
a.key = a // 环引用
const b = cloneDeep(a)
console.log(b)
console.log(b === a) // false