今天和大家一起来探讨一下javascript中的拷贝,使用拷贝的情况,要根据javascript的数据类型来定,javascript的数据类型分为基础类型和引用类型,只有引用类型才会用到拷贝。
而引用类型中拷贝用的最多是对象类型。
而拷贝的层次又分为两种,浅拷贝与深拷贝:
首先是浅拷贝只会将对象的各个属性进行依次复制,并不会进行递归复制,也就是说只会复制目标对象的第一层属性。
深拷贝不同于浅拷贝,它不只拷贝目标对象的第一层属性,而是递归拷贝目标对象的所有属性。
浅拷贝对于目标对象第一层为基本数据类型的数据,就是直接赋值,即「传值」;而对于目标对象第一层为引用数据类型的数据,就是直接赋存于栈内存中的堆内存地址,即「传址」,并没有开辟新的栈,也就是复制的结果是两个对象指向同一个地址,修改其中一个对象的属性,则另一个对象的属性也会改变。
深拷贝的复制则是开辟新的栈,两个对象对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性。
一句话总结,浅拷贝对于第一层属性值为引用类型数据的时候,不做处理,直接复制引用地址,深拷贝会开辟新的栈,生成新的数据。
先来看一下浅拷贝的几种实现方式:
第一种 for in循环,代码如下:
var obj = {
name:"zs",
age:"18",
gender:"男",
hobby:["吃饭","唱歌","爬山"]
}
var newobj = {};
for(var key in obj){
newobj[key] = obj[key];
}
console.log(newobj);
newobj.hobby.push("旅游");
console.log(obj)
打印结果:
可以看到实现了浅层的拷贝,当第一层的属性值为引用类型是,在拷贝时只是赋值了一个引用。修改newobj的hobby属性,obj的hobby属相也会发生变化。
接着看第二种实现方式,这次使用object.assign,代码如下:
var obj = {
name:"zs",
age:"18",
gender:"男",
hobby:["吃饭","唱歌","爬山"]
}
var newobj = Object.assign({},obj)
console.log(newobj);
newobj.hobby.push("旅游")
console.log(obj)
打印结果:
查看输出结果发现同样实现了浅拷贝。
接着看第三种浅拷贝的实现方式,扩展运算符:
var obj = {
name:"zs",
age:"18",
gender:"男",
hobby:["吃饭","唱歌","爬山"]
}
var newobj = {...obj}
console.log(newobj);
newobj.hobby.push("旅游")
console.log(obj)
打印结果:
以上的案例都是对象的浅拷贝的实现,除了对象还有数组,数组的浅拷贝有如下几种方式,for循环、concant、slice这里就不一一举例了。
说完浅拷贝来看一下深拷贝的实现。
先看第一种实现方式,利用JSON.parse和JSON.stringify,代码如下:
var obj = {
name:"zs",
age:"18",
gender:"男",
hobby:["吃饭","唱歌","爬山"]
}
var newobj = JSON.parse(JSON.stringify(obj));
// 修改newobj中的hobby
newobj.hobby.push("旅游")
// 观察obj中的hobby的变化。
console.log(obj)
打印结果如下:
从结果我们可以看出,这次实现了深度拷贝,newobj上的hobby新开辟了一个空间,修改后不会影响obj的hobby属性了。
但是这种拷贝方式有几点问题需要注意:
只要碰到上面四种情况便不能使用JSON来实现深拷贝了。
第二种深拷贝的实现方式我们可以使用MessageChannel,代码如下:
function deepClone(obj) {
return new Promise(resolve => {
var channel = new MessageChannel()
channel.port2.onmessage = ev => resolve(ev.data)
channel.port1.postMessage(obj)
})
}
var obj = {
a: 1,
b: {
c: 2
}
}
// 添加一个循环引用;
obj.z = obj;
//再添加一个循环引用;
obj.b.d = obj.b
// 注意该方法是异步的
// 可以处理 undefined 和循环引用对象
const test = async () => {
const clone = await deepClone(obj)
console.log(clone)
}
test()
使用MessageChannel需要注意的是这种处理方式是异步的,它可以处理内置类型,处理内部循环引用,比第一种更加强大,基本所有的深拷贝都能完成,不过可惜只能在浏览器端,只是不能处理函数。
最后一种就是用递归的方式实现深拷贝,但是自己实现的话需要考虑好多边界情况,例如碰见javascript内置对象(Date,正则)如何处理,碰见数组如何处理,碰见循环引用如何处理,等等。
我们先简单的实现一版,在进一步迭代,代码如下:
function copy(obj) {
let res;
if (typeof obj === "object") {
let newobj = {};
for (const key in obj) {
newobj[key] = copy(obj[key])
}
res = newobj
} else {
res = obj;
}
return res
}
仔细观察上面代码,实现简单对象的克隆不成问题,但是并没有对数组、时间对象、正则对象进行处理,如果克隆的数据包含内置对象就不能满足需求了,所以我们需要处理这些边界情况,代码更改如下:
function copy(obj) {
let res;
if (typeof obj === "object") {
if (Object.prototype.toString.call(obj) == "[object Date]") {
res = obj;
console.log("obj")
}
else if (Object.prototype.toString.call(obj) == "[object RegExp]") {
res = obj;
}
else if (Object.prototype.toString.call(obj) == "[object Array]") {
let arr = [];
for (let i = 0; i < obj.length; i++) {
arr[i] = copy(obj[i])
}
res = arr;
}
else if (Object.prototype.toString.call(obj) == "[object Object]") {
let newobj = {};
for (const key in obj) {
newobj[key] = copy(obj[key])
}
res = newobj
}
} else {
res = obj;
}
return res
}
// 测试代码
var obj = {
a:1,
b:{name:2,hobby:[1,2,3,4]},
c:"333",
t:new Date()
}
let newobj = copy(obj);
obj.b.hobby.push(22222);
console.log(newobj);
上面的代码我们使用了Object.prototype.toString.call(obj)来判断数据的类型,从而对边界进行了处理。面试的时候也是主要考察参见面试人员如何对边界情况进行处理。