数据分为基本数据类型(String, Number, Boolean, Null, Undefined,Symbol)和引用数据类型Object,包含(function,Array,Date)。
1、基本数据类型的特点:直接存储在栈内存中的数据
2、引用数据类型的特点:存储的是该对象在栈中引用,真实的数据存放在堆内存里
引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
通过栈里面定义一个地址值,通过地址值去找堆里面定义的某一个值,
很重要一点是他的栈里是个地址值,地址值指向的是堆,他在堆里面定义的某一个值
相当于拿着号,去堆里面去找,两个号也就是地址值其实是一模一样的堆跟栈最大的区别是:
1)堆在栈里存了一个地址值
2)栈存储的永远是一个基本数据类型的数据
浅拷贝
:创建一个新对象,这个对象有着原始对象属性值的一份精准拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是引用类型,拷贝的就是内存地址。这个内存地址指向同一个堆内存
。如果其中一个对象改变了这个地址,就会影响到另一个对象。
深拷贝
:创建一个新对象,如果属性是基本类型,拷贝的就是基本类型的值;如果属性是引用类型,则从堆内存
中开辟一个新的区域存放该引用类型指向的堆内存中的值,修改新对象的值不会影响原对象。
浅拷贝和深拷贝引用其他博主图片,示意图大致如下:
总之,浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会修改到原对象。
const obj1 = {
name: "icy",
age: 20,
hobbies: ["eat", "sleep", "game"],
};
const obj2 = { ...obj1 }
console.log(obj2, 'obj2')
打印结果
为了验证是浅拷贝,我们改变一下obj2中数组的第一项的值,然后再输出ojb1和obj2:
const obj1 = {
name: 'Nancy',
age: 18,
hobbies: ['eat', 'sleep', 'game']
}
const obj2 = { ...obj1 }
// 修改堆内存中的值
obj2.age = 20
obj2.hobbies[0] = 'play'
console.log('修改后obj2', obj2)
console.log('修改前obj1', obj1)
打印结果如下:
修改数组时候,obj1和obj2都受到了影响,验证了浅拷贝。
Object.assign()
方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象,返回值为合并后的新对象
。但是Object.assign() 进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。
注意: 当object只有一层的时候,是深拷贝。
let obj = { username: 'kobe' }
let obj2 = Object.assign({}, obj) //将拷贝对象与{}空对象合并
obj2.username = 'wade'
console.log(obj,'obj')
console.log(obj2,'obj2')
该方法用于数组合并,合并的数组.concat(被合并的数组…)
参数可有多个,用逗号分隔,返回合并后的数组。
原理:用原数组去合并一个空数组,返回合并后的数组。
let arr = [1, 3, {
username: 'kobe'
}];
let arr2 = arr.concat();
arr2[2].username = 'wade';
arr2[0] = 555;
console.log(arr,'arr');
console.log(arr2,'arr2');
该方法用途很多,可对数组进行增删,剪裁操作。
const arr1 = [1, 3, { username: 'kobe' }]
const arr2 = arr1.slice() //返回剪裁后的数组,这里没有剪裁掉任何项,相当于返回原数组
// 修改堆内存中的值
arr2[0] = 5
arr2[2].username = 'wade'
console.log('arr1', arr1)
console.log('arr2', arr2)
关于Array的slice和concat方法的补充说明:Array的slice和concat方法不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。
个人感觉,以上四种方法都只有一层的时候才是深拷贝。
创建一个新的对象
,遍历需要克隆的对象,将需要克隆对象的属性依次添加到新对象上,返回。
function clone(target) {
let cloneTarget = {};
for (const key in target) {
cloneTarget[key] = target[key];
}
return cloneTarget;
};
原理:用JSON.stringify将对象转成JSON字符串,再用JSON.parse()把字符串解析成对象,一去一来,新的对象
产生了,而且对象会开辟新的栈,实现深拷贝。
const obj1 = {
user_info:{
name: "Nancy",
age: 18,
gender: "女",
},
hobbies: ["eat", "sleep", "game"],
}
const obj2 = JSON.parse(JSON.stringify(obj1));
obj2.user_info.name = 'Juliet'
console.log(obj1,'obj1');
console.log(obj2,'obj2')
注意点:
这种方法虽然简单方便,可以实现数组或者对象的深拷贝。但是不能处理函数
和正则
,因为这两者基于JSON.stringify 和 JSON.parse处理后,得到的正则就不再是正则(变为空对象),得到的函数就不再是函数(变为null)了。
const obj1 = {
name: "Nancy",
age: 18,
gender: "女",
hobbies: ["eat", "sleep", "game"],
//函数
watchComic: () => {
console.log("Nancy 你好");
},
//正则
regx: /^icy{3}$/g,
};
const obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj2,'obj2');
可看到上图打印,函数没了,正则变成空对象{}。
这是因为 JSON.stringify()
方法是将一个JavaScript值(对象或者数组)转换为一个 JSON字符串,不能接受函数。
递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。
手动递归实现深拷贝,我们只需要完成以下2点即可:
- 对于基本数据类型,我们只需要简单地赋值即可(使用 “=” )
- 对于引用类型,我们需要创建新的对象,并通过遍历键来赋值对应的值,这个过程中如果遇到 Object 类型还需要再次进行遍历。(注意Array也属于Object类型)
export function deepCopy (obj1, obj2) {
// 深拷贝
if (obj2 === undefined) {
if (Array.isArray(obj1)) {
obj2 = []
} else {
obj2 = {}
}
}
// 遍历目标数据
for (let i in obj1) {
if (obj1.hasOwnProperty(i)) {
if (Array.isArray(obj1[i])) { //判断目标结构里的每一项值是否存在数组/对象
obj2[i] = []
deepCopy(obj1[i], obj2[i])
} else if (typeof obj1[i] === 'function') {
obj2[i] = obj1[i]
} else if (obj1[i] instanceof Object) {
obj2[i] = {}
deepCopy(obj1[i], obj2[i])
} else {
obj2[i] = obj1[i] // 这里的值就是基本数据类型了
}
}
}
return obj2
}
const obj1 = {
user_info: {
name: 'Nancy',
age: 18,
gender: '女'
},
hobbies: ['eat', 'sleep', 'game'],
// 函数
watchComic: () => {
console.log('Nancy 你好')
}
}
let obj2 = deepCopy(obj1)
obj2.user_info.age = 20
obj2.user_info.gender = '男'
console.log(obj1, 'obj1')
console.log(obj2, 'obj2')
学习过程中也遇到一些很不错的文章:
彻底讲明白浅拷贝与深拷贝
前端深拷贝与浅拷贝(附实现方法)
谁动了我的数据 | 程序员必备小知识