JS的深浅拷贝
一、数据类型
数据分为基本数据类型(String, Number, Boolean, Null, Undefined,Symbol)和 引用数据类型(统称为 Object类型,细分的话有:Object 、Array 、Date 、RegExp 、Function… )。
基本数据类型的特点:直接存储在栈(stack)内存中
引用数据类型的特点:真实的数据存放在堆内存中,在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
基本数据类型是不可变的,任何方法都无法改变一个基本数据类型的值,也不可以给基本数据类型添加属性或方法。但是可以为引用数据类型添加属性和方法,也可以删除其属性和方法。
为什么要分两种保存方式呢? 因为保存在栈内存的必须是大小固定的数据,引用类型的大小不固定,只能保存在堆内存中,但是我们可以把它的地址写在栈内存中以供我们访问。
二、区分浅拷贝与深拷贝
拷贝其实就是对象复制,为了解决对象复制是产生的引用类型问题。
浅拷贝:利用迭代器,循环对象将对象中的所有可枚举属性复制到另一个对象上,但是浅拷贝有一个问题就是只拷贝了对象的一级,其他级如果还是引用类型的话依旧解决不了问题
深拷贝:深拷贝解决了浅拷贝的问题,利用递归的形势遍历对象的每一级,实现起来较为复杂,得判断值是数组还是对象,简单的说就是,在内存中存在两个数据结构完全相同又相互独立的数据,将引用类型的值进行复制,而不是只复制其引用关系,能够实现真正意义上的对象的拷贝。
三、赋值和浅拷贝的区别
当我们把一个对象赋值给一个新的变量时,赋的其实是该对象在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即:默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。
我们先来看两个例子,对比一下赋值与浅拷贝分别会对原对象带来哪些改变
对象赋值:
var obj1 = { name:'zhang', msg:{ age:18 } } var obj2 = obj1 obj2.name = 'wang' obj2.msg.age = 20 console.log('obj1',obj1) console.log('obj2',obj2)

浅拷贝:
var obj1 = {
name:'zhang', msg:{ age:18 } } var obj3 = shallowCopy(obj1) obj3.name = 'wang' obj3.msg.age = 20 function shallowCopy(obj) { var res = Array.isArray(obj) ? [] : {}
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
res[key] = obj[key]
}
}
return res
}
console.log('obj1',obj1)
console.log('obj3',obj3)

上面例子中,obj1是原始数据,obj2是赋值操作得到的,而obj3是浅拷贝得到的。我们可以很清晰的看到对原始数据的影响。
四、浅拷贝的实现方式
1.Object.assign( )、Object.create( )、Object解构
Object.assign() 方法可以把任意多个源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。但是 Object.assign()进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。
Object.assign():
var obj = { a: {name: 'zhang', age: 30} }
var newObj = Object.assign({}, obj)
newObj.a.name = "wang"
console.log(obj.a.name)//wang
Object.create():
var obj = { a: {name: 'zhang', age: 30} }
var newObj = Object.create(obj)
newObj.a.age = 25
console.log(obj.a.age)//25
解构:
var obj = { a: {name: 'zhang',sex:'男'} }
var newObj = {...obj}
newObj.a.sex = '女'
console.log(obj.a.sex)//女
注意:当object只有一层的时候,是深拷贝
Object.assign():
var obj = { name: "zhang" }
var newObj = Object.assign({}, obj)
newObj.name = "wang"
console.log(obj.name)//zhang
Object.create():
var obj = { age: 20 }
var newObj = Object.create(obj)
newObj.age = 30
console.log(obj.age)//20
解构:
var obj = { sex: '男' }
var newObj = {...obj}
newObj.sex = '女'
console.log(obj.sex)//男
2.Array.prototype.concat( )、Array.prototype.slice( )
修改新对象会改到原对象:
concat():
var arr1 = [1, 2, { name: 'zhang' }]
var arr2 = arr1.concat()
arr2[2].name = 'wang'
console.log(arr1[2].name)//wang
slice():
var arr1 = [1, 2, { name: 'zhang' }]
var arr2 = arr1.slice()
arr2[2].name = 'wang'
console.log(arr1[2].name)//wang
Array的slice和concat方法不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。
原数组的元素会按照下述规则拷贝:
- 如果该元素是个对象引用(不是实际的对象),slice 会拷贝这个对象引用到新的数组里。两个对象引用都引用了同一个对象。如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变。
- 对于字符串、数字及布尔值来说(不是 String、Number 或者 Boolean 对象),slice 会拷贝这些值到新的数组里。在一个数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组。(若拷贝数组是纯数据(不含对象),可以通过concat() 和 slice() 来实现深拷贝)
五、深拷贝的实现方式
1.通过 JSON 反序列化来实现:JSON.parse(JSON.stringify( ))
原理: 用JSON.stringify将对象转成JSON字符串,再用JSON.parse()把字符串解析成对象,一去一来,新的对象产生了,而且此对象会开辟新的栈,实现深拷贝。
var arr1 = [1, 2, { name: 'zhang' }]
var arr2 = JSON.parse(JSON.stringify(arr1))
arr2[2].name = 'wang'
console.log(arr1[2].name)//zhang
缺点:因为JSON.stringify() 方法是将一个JavaScript值(数组或对象)转换为一个 JSON字符串,所以这种方法只能实现数组和对象的深拷贝,而不能处理函数和其他数据类型
2.手写递归克隆方法
递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝
通过JSON 反序列化进行深拷贝除了不支持数组和对象以外的数据类型,也不能处理内部循环引用的问题,先来看一下什么是循环引用:
var obj = {a: '123'} obj.b = obj
为了解决循还引用,我们建立一个集合[],每次遍历对象进行比较,如果[]中已存在,则证明出现了循环引用或者相同引用,我们直接返回该对象已复制的引用即可
class DeepClone {
constructor() {
this.cacheList = []
}
clone(target) {
if(typeof target !== 'object'){
return target
}
const cache = this.findCache(target)
if (cache) {
return cache // 如果找到缓存,证明存在循环引用或属性引用了相同对象,直接返回引用就可以了
} else {
if(target instanceof Array){
var result = []
}else{
var result = {}
}
this.cacheList.push([target, result])// 把源对象和新对象放进缓存列表
for (let key in target) {
if (target.hasOwnProperty(key)) { // 不拷贝原型上的属性,太浪费内存
if(typeof target[key] !== 'object'){
result[key] = target[key] // 不是引用类型,则复制值
}else{
result[key] = this.clone(target[key]) //递归遍历复制对象
}
}
}
return result
}
}
findCache(source) {
for (let i = 0; i < this.cacheList.length; i++) {
if (this.cacheList[i][0] === source) {
return this.cacheList[i][1]; // 如果有环,返回对应的新对象
}
}
return undefined;
}
}
var obj1 = {
name:'zhang',
msg:{
age:18
}
}
var deepClone = new DeepClone()
var obj2 = deepClone.clone(obj1)
obj2.name = 'wang'
obj2.msg.age = 20
console.log('obj1',obj1)
console.log('obj2',obj2)

3.函数库lodash
该函数库提供了_.cloneDeep用来做深拷贝
var _ = require('lodash')
var obj1 = {a: { a: 10 }}
obj2 = _.cloneDeep(obj1)
obj2.a.a = 20
console.log(obj1.a.a)//10
总结:简单需求用 JSON 反序列化,复杂需求用递归克隆或lodash函数库。

浙公网安备 33010602011771号