在搞明白浅拷贝与深拷贝之前,我们先回顾一下基础问题。
1、js有几种数据类型?
数据类型分为两种:基本数据类型(String, Number, Boolean, Null, Undefined,Symbol)和引用数据类型。
2、js的赋值,是值传递还是引用传递?
这个大多分为两种理解:
理解一、
基本数据类型的赋值是值传递,而引用数据类型的赋值是引用赋值:
基本类型存放在栈区,访问时按值访问,赋值是按照普通方式赋值;
var a = '路途虽远'
var b = '行则将至'
var c = a + ',' + b
console.log(c) //路途虽远,行则将至
引用类型的存储方式为:在栈中存放对象变量标示名称和该对象所对应在堆内存中的地址值,数据全部存放在堆内存中。当我们把一个对象赋值给一个新的变量时,赋的其实是该对象在堆中的地址值,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,更改任意一个都会改变另外一个。
var d = ['one','two']
var e = d
e[0] = '一'
console.log(d) // ['一', 'two']
理解二、
js的所有赋值都是值传递,只是有时候赋的值是真实值,有时候是内存地址。
这两种理解都是正确的,我更倾向于第二种,看完问题2可能会有基础不好的小伙伴不太明白什么是栈内存什么是堆内存。可以简单了解一下另外一篇文章:什么是栈内存,什么是堆内存
浅拷贝与深拷贝
浅拷贝和深拷贝是只针对Object和Array这样的引用数据类型。
浅拷贝直接复制对象的地址值,而不复制对象在堆内存中的数据,新旧对象还是共享同一块堆内存,修改任一对象都会改变另外一个对象。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会影响到原对象。
浅拷贝与深拷贝的实现方式
浅拷贝:
1.Object.assign()
Object.assign() 是最常用的一种方法,用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。
语法:Object.assign(target,...aources)
参数:target:目标对象 aources:源对象
返回值:目标对象
let a = {
name: 'Bob',
motto: "行则将至",
obj: {
age: 18
}
}
let b = {}
Object.assign(b, a);
console.log(b); //{name: 'Bob',motto: "行则将至",obj: {age: 18}}
因为浅拷贝对引用类型拷贝的是地址值,所以当修改任一对象另外一个的值也会改变。
a.name = "Jack";
a.obj.age = 20;
console.log(a); //{name: 'Jack',motto: "行则将至",obj: {age: 20}}
console.log(b); //{name: 'Bob',motto: "行则将至",obj: {age: 20}}
2.slice()
方法返回一个新的数组对象,这一对象是一个由 start 和 end 决定的原数组的浅拷贝(包括 start ,不包括 end )。原始数组不会被改变。
语法:array.slice(start,end)
参数:start:提取起始处索引 end:提取结束处索引
返回值:一个含有被提取元素的新数组
var arr = ['one','two',['three'],{name:'Bob'}]
var b = arr.slice()//不传任何参数默认从0开始到结尾。
arr[0] = '一'
arr[2][0] = '三'
arr[3].name = 'Jack'
console.log(arr);// ['一', 'two',['三'],{name: 'Jack'}]
console.log(b);// ['one', 'two',['三'],{name: 'Jack'}]
3.concat()
方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。
语法:new_arr = old_arr.concat(value1[, value2[, ...[, valueN]]])
参数:value数组或值。如果省略了所有 valueN 参数,则 concat 会返回调用此方法的现存数组的一个浅拷贝。
返回值:新得数组
这里就不再写代码了,用法跟slice()一致。
4.数组的解构
这个方法挺有趣的,前几天做项目的时候突然想到解构也可以把值重新赋给一个新得变量,验证了一番之后发现这个也是浅拷贝,所以在这里也记下来。
var arr = ['one','two',['three'],{name:'Bob'}]
var b = [...arr]
arr[0] = '一'
arr[2][0] = '三'
arr[3].name = 'Jack'
console.log(arr);// ['一', 'two',['三'],{name: 'Jack'}]
console.log(b);// ['one', 'two',['三'],{name: 'Jack'}]
深拷贝:
1.JSON.parse(JSON.stringify())
其过程就是利用JSON.stringify 将js对象序列化(JSON字符串),再使用JSON.parse来反序列化(还原)js对象,这个属于比较常用的一种深拷贝的方法
var arr = ['one','two',['three'],{name:'Bob'}]
var b = JSON.parse(JSON.stringify(arr))
arr[0] = '一'
arr[2][0] = '三'
arr[3].name = 'Jack'
console.log(arr);// ['一', 'two',['三'],{name: 'Jack'}]
console.log(b);// ['one', 'two',['three'],{name: 'Bob'}]
可以看出来,修改arr的任何一个索引都对深拷贝后的 b 没有任何影响,但是这种方法也有个问题需要特别注意一下:使用JSON.parse(JSON.stringify(obj))进行深拷贝时的注意事项
2.手写递归实现深拷贝
递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去赋值,就是深度拷贝。
// 定义检测数据类型的功能函数
function checkedType(target) {
return Object.prototype.toString.call(target).slice(8, -1)
}
// 实现深度克隆---对象/数组
function deepClone(target) {
// 初始化变量result,并拿到要拷贝的数据类型
let result, targetType = checkedType(target)
console.log(targetType)
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') {
// 继续遍历获取到value值
result[i] = deepClone(value)
} else {
// 获取到value值是基本的数据类型或者是函数。
result[i] = value;
}
}
return result
}
var arr = ['one','two',['three'],{name:'Bob'}]
var b = deepClone(arr)
arr[0] = '一'
arr[2][0] = '三'
arr[3].name = 'Jack'
console.log(arr);// ['一', 'two',['三'],{name: 'Jack'}]
console.log(b);// ['one', 'two',['three'],{name: 'Bob'}]
这样就实现了一个递归深拷贝,在项目中也可以直接将这个方法添加到vue的原型中来全局调用。