代码实现深拷贝
经过对浅拷贝的简单了解,咱们就到了如何实现深拷贝的问题上,常见的循环遍历,只是遍历了一层数据,明显解决不了这个问题,不过咱们可以看出,深拷贝的问题不就在于怎么解决无限层级拷贝问题吗,这种数据类型似乎在哪见过,对没错,可以用递归解决!
咱们先对浅拷贝代码进行改造,用了递归之后就实现了一个深拷贝。
const deepClone = (source) => {
const target = {};
for (const i in source) {
if (source.hasOwnProperty(i)
&& target[i] === 'object') {
target[i] = deepClone(source[i]); // 注意这里
} else {
target[i] = source[i];
}
}
return target;
};
但是这份代码还有一些细节需要修改,如:
-
没有对参数进行校验,如果传入进来的不是对象或者数组,我们直接返回即可。
-
通过 typeof 判断是否对象的逻辑不够严谨。 typeof null
对此可进行改进,实现一份完整的深拷贝代码。
1. 手写深拷贝
// 定义检测数据类型的功能函数
const checkedType = (target) => {
// console.log(Object.prototype.toString.call(target))
console.log(Object.prototype.toString.call(target));
return Object.prototype.toString.call(target).slice(8, -1);
}
// 实现深度克隆对象或者数组
const deepClone = (target) => {
// 判断拷贝的数据类型
// 初始化变量 result 成为最终数据
let result, targetType = checkedType(target);
if (targetType === 'Object') {
result = {};
} else if (targetType === 'Array') {
result = [];
} else {
return target;
}
// 遍历目标数据
for (let i in target) {
// 获取遍历数据结构的每一项值
let value = target[i];
// 判断目标结构里的每一项值是否存在对象或者数组
if (checkedType(value) === 'Object' || checkedType(value) === 'Array') {
// 如果对象或者数组中还嵌套了对象或者数组,那么继续遍历
result[i] = deepClone(value);
} else {
result[i] = value;
}
}
// 返回最终值
return result;
}
const obj1 = [
1,
'Hello!',
{ name: '张' },
[
{
name: '李',
}
],
]
const obj2 = deepClone(obj1);
obj2[0] = 2;
obj2[1] = 'Hi!';
obj2[2].name = '小张';
obj2[3][0].name = '小李';
console.log(obj1);
// [ 1, 'Hello!', { name: '张' }, [ { name: '李' } ] ]
console.log(obj2);
// [ 2, 'Hi!', { name: '小张' }, [ { name: '小李' } ] ]
经过深拷贝后,我们可以看到,再修改引用数据类型里的值时,原对象毫不收到影响,说明这份代码还是挺成功的。下面是代码的实现思路:
-
Object.prototype.toString.call()
:稳健地判断JavaScript
数据类型方式,可以符合预期的判断基本数据类型String
、Undefined
等,也可以判断Array
、Object
这些引用数据类型。 -
然后,我们通过方法
targetType()
中的Object.prototype.toString.call()
,判断传入的数据类型属于那种,从而改变result
的值为 {}、[] 或者直接返回传入的值(return target)
。 -
最后,我们再通过
for...in
判断target
的所有元素,如果属于 {} 或者 [],那么就递归再进行clone()
操作; -
如果是基本数据类型,则直接传递到数组中……从而在最后返回一个深拷贝的数据。
但是如果经过严格的测试,这份代码也是行不通的,如果拷贝对象是个循环空对象,那么咱们将把自己绕进去,写了个死循环。而对广度和深度进行测试后,数据大的话,也是行不通的,十分容易爆栈。
这就是手写深拷贝待解决的两个大问题:
- 死循环
- 爆栈
解决方法,博主也是了解的比较少,能简单理解到:
- 死循环的解决是用
哈希表
进行循环检测,我们设置一个数组或者哈希表存储已拷贝过的对象,当检测到当前对象已存在于哈希表中时,取出该值并返回即可。 - 爆栈的问题可用栈解决
这里就不过多解释了,有兴趣的可去大佬文章深挖。
2. JSON. parse (JSON. stringify())
JSON. stringify()
:将对象转成JSON 字符串。JSON. parse()
:将字符串解析成对象。
const arr1 = [
1,
{
username: 'zhangsan',
},
];
let arr2 = JSON.parse(JSON.stringify(arr1));
arr2[0] = 2;
arr2[1].username = 'li';
console.log(arr1);
// [ 1, { username: 'zhangsan' } ]
console.log(arr2);
// [ 2, { username: 'li' } ]
通过JSON. parse (JSON. stringify())
将JavaScript
对象转序列化(转换成JSON
字符串),再将其还原成JavaScript对象,一去一 来我们就产生了一个新的对象,而且对象会开辟新的栈,从而实现深拷贝。
此方法虽然简单,但是却有很多局限性。
- 1、不能存放
函数
或者Undefined
,否则会丢失函数
或者Undefined
; - 2、不要存放时间对象,否则会变成字符串形式;
- 3、不能存放
RegExp
、Error
对象,否则会变成空对象; - 4、不能存放
NaN
、Infinity
、-Infinity
,否则会变成null
;
3.函数库Lodash
Lodash
作为一个JavaScript
函数库/工具库,它里面有非常好用的封装好的功能,大家可以去试试,这里我们查看下它的cloneDeep()
方法,该方法会递归拷贝value
。
首先需要npm
先下载lodash
包
var lodash = require('lodash');
const obj1 = [
1,
'Hello!',
{ name: 'html' },
[
{
name: 'css',
}
],
]
const obj2 = lodash.cloneDeep(obj1);
obj2[0] = 2;
obj2[1] = 'Hi!';
obj2[2].name = 'lodash';
obj2[3][0].name = 'js';
console.log(obj1);
// [ 1, 'Hello!', { name: 'html' }, [ { name: 'css' } ] ]
console.log(obj2);
// [ 2, 'Hi!', { name: 'lodash' }, [ { name: 'js' } ] ]
4.框架jQuery中extend() 方法
const obj1 = [
1,
'Hello!',
{ name: 'html' },
[
{
name: 'css',
}
],
]
const obj2 = {};
/**
* @name jQuery深拷贝
* @description $.extend(deep, target, object1, object2...)
* @param {Boolean} deep 可选 true 或者 false,默认是 false,所以一般如果需要填写,最好是 true。
* @param {Object} target 需要存放的位置
* @param {Object} object 可以有 n 个原数据
*/
$.extend(true, obj2, obj1);
obj2[0] = 2;
obj2[1] = 'Hi!';
obj2[2].name = 'js';
obj2[3][0].name = 'jq';
console.log(obj1);
console.log(obj2);
总结
之前浏览博客的时候,总是看到别人关于深拷贝与浅拷贝的文章,也知道面试这是常问的知识点,但是一直没怎么看。而在前不久,在小组轮到我讲课了,思来想去讲写什么,最后决定讲这个,经过一星期的学习,真的学习到了不少,碰到任何不懂的问题,自己立马去搜了搜,也连联想了许多相关的知识点,也算是把这一难点学会了,虽然平时可能用不用到,但用不用的到并不是关键,要的是学会知识😁,一个深拷贝却蕴含了那么多的知识点,谁能想的到,平时可能也不会怎么注意吧。
完整文章见: