前言
先聊一下关于数据的存储,数据分为两大类:原始数据类型和引用数据类型,原始数据类型有七类:number、string、boolean、undefined、null、Symbol、BigInt;引用数据类型有:function、{}、date、[]等。那么他们的存储方式有什么区别呢?
- 原始数据类型,js引擎会将它直接存储在栈里面
- 引用数据类型:js引擎会将它的引用地址存储在栈里,值存储在堆里
代码实例一:
ini
let a = 1;
let b = a;
a = 2;
console.log(b);
分析: 原始类型的值,js引擎会将它直接存储在栈里面,所以定义一个数字类型的变量a,js引擎会在栈中为它分配一个内存空间,将a赋值给另外一个变量b时,js引擎会在栈中为b分配一个内存空间,并将a的值分配给b,所以修改a的值不会影响b。
代码实例二:
ini
let a = {
name:'ltt'
}
let b = Object.create(a);
a.name = 'lxp';
console.log(b.name);
分析: 引用类型的值,js引擎会在堆中为其分配一段内存空间,然后将地址存放在栈中,所以存放在栈中的为a的地址,创建b,将a赋值给b,复制的为a的地址。所以当a发生改变的时候,b也会发生改变。
正文
浅拷贝(Shallow Copy)
定义
创建一个新对象或数组作为原对象或数组的副本,但这个副本与原对象或数组中的引用类型成员(如子对象、数组等)共享相同的内存地址 。换言之,浅拷贝仅仅复制了对象的第一层(顶层)的值,如果对象的属性是基本数据类型(如数字、字符串、布尔值等),这些值会被复制;而如果属性是引用类型(如对象、数组),则复制的是这些引用的地址而非实际的引用对象。因此,原对象和副本对象中的引用类型成员是相互影响的,修改其中一个对象的引用类型成员会影响到另一个对象。
常见的浅拷贝方法
Object.create(x)
create()创建一个新对象,将a对象复制给该新对象,为浅拷贝,a为引用类型,所以将a赋值给b只是复制了该对象的第一层,即a、b共享相同的内存地址,所以修改a的值,b也会受影响:
ini
let a = {
name:'ltt'
}
let b = Object.create(a);
a.name = 'lxp';
console.log(b.name);
Object.assign({}, a)
css
Object.assign(target, ...sources)
target
:目标对象,即要被赋值的对象。...sources
:源对象,一个或多个对象,它们的可枚举属性会被复制到目标对象中。
示例
assign()将对象a复制给目标对象{},为浅拷贝,name为原始数据类型,值会被直接复制,like为引用数据类型,复制的为like的第一层,即like的内存地址,所以修改name不会影响c,修改like会影响c:
ini
let a = {
name:'ltt',
like: {
n:'素~'
}
}
let c = Object.assign({}, a);
a.name = 'lxp';
a.like.n = '亚比,囧囧囧~';
console.log(c);
[].concat(arr)
通过concat将arr与[]连接起来,返回一个新数组,也就相当于把arr复制给newArr,这种复制的方式为浅拷贝,1,2,3
都为原始数据类型,会直接复制,所以当修改arr的时候,newArr不会发生改变。
ini
let arr = [1, 2, 3];
let newArr = [].concat(arr);// 将arr中的元素和[]的元素合并,并返回到一个新数组中
arr.splice(1, 1);
arr[1] = 20;
console.log(newArr);
数组解构 [...arr]
[...arr]将arr复制给newArr,这种复制为浅拷贝,1,2,3
为原始数据类型,会直接复制,[4]
为引用数据类型,会把第一层复制给newArr,即该对象的内存地址,所以改变arr[2]不会影响,改变arr[3][0]会影响。
ini
let arr = [1, 2, 3, [4]];
let newArr = [...arr];
arr[2] = 30;
arr[3][0] = 40;
console.log(newArr);
arr.slice(0)
sql
array.slice([start[, end]])
start
:提取开始的索引(包含该位置的元素)。如果为负数,则表示从数组末尾开始计算的位置。默认为0。end
:提取结束的索引(不包含该位置的元素)。如果省略或为数组长度,则提取至数组末尾。如果为负数,同样表示从数组末尾开始计算的位置。
arr.slice(0)表示从数组的下标0开始,提取至数组末尾,也就是将数组arr复制给s,这种复制为浅拷贝,1,2,3
为原始数据类型,直接复制,修改arr中的这些数据,不会影响s,{a:10}
为引用数据类型,复制第一层,即该对象的内存地址,所以修改arr中的该对象会影响s。
ini
let arr = [1, 2, 3, {a: 10}]
let s = arr.slice(0);
arr[1] = 20;
arr[3].a = 100;
console.log(s);
console.log(arr);
arr.toReversed().reverse()
arr.toReversed()
的效果是得到一个与原数组元素顺序相反的新数组,而原数组保持不变,然后通过调用 reverse()
反转数组,使其顺序回到最初的状态,也就达到了将arr复制给s,这种复制为浅拷贝。
ini
let arr = [1, 2, 3, {a: 10}]
let s = arr.toReversed().reverse();
arr[1] = 20;
arr[3].a = 100;
console.log(s);
console.log(arr);
如果终端出现下面的错误,是因为node版本低了,可以去控制台运行查看结果
手写浅拷贝
for in会遍历到隐式属性:
ini
Object.prototype.d = 4;
let obj = {
a: 1,
b: 2,
c: 3
}
for (let key in obj) {
console.log(obj[key]);
}
创建一个新对象newObj,通过for in循环遍历obj,将其复制给newObj,for in会遍历到obj的隐式属性,所以在复制给newObj之前要先通过obj.hasOwnProperty(key)检查是不是obj自己具有的属性。
ini
let obj = {
name: 'ltt',
like: {
a: 'eat'
}
}
function shadow(obj) {
let newObj = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key];
}
}
return newObj;
}
let newObj = shadow(obj);
obj.name = 'lxp';
obj.like.a = 'running';
console.log(newObj);
实现原理
- 借助for in 遍历原对象,将原对象的属性值增加到新对象中
- 因为for in 会遍历到对象隐式具有的属性,通常要使用
- obj.hasOwnProperty(key)来判断要拷贝的属性是不是对象显示具有的
深拷贝(Deep Copy)
定义
深拷贝则是创建一个完全独立的新对象或数组 ,这个新对象不仅复制了原对象的所有第一层属性值,而且递归地复制了所有嵌套的引用类型的属性,直到所有的层级都是全新的副本 。这意味着深拷贝的对象与原对象在内存中是完全独立的,即使内部包含复杂的数据结构,修改深拷贝后的对象也不会影响到原对象及其子对象。
常见的浅拷贝方法
JSON.parse(JSON.stringify(obj))
这种复制方式为深拷贝,拷贝后的新对象与原对象互相独立,所以修改原对象不会影响新对象
javascript
let obj = {
name: 'ltt',
age: 18,
like: {
n:'run'
},
a: true,
b: undefined,
c:null,
d: Symbol(1),
f: function() {}
}
let obj2 = JSON.parse(JSON.stringify(obj));
obj.like.n = 'eat';
console.log(obj);
console.log(obj2);
当进行循环引用的时候会报错:
ini
let obj = {
name: 'ltt',
age: 18,
like: {
n:'run'
},
a: true,
b: undefined,
c:null,
d: Symbol(1),
f: function() {}
}
obj.c = obj.like;
obj.like.m = obj.c;
let obj2 = JSON.parse(JSON.stringify(obj));
obj.like.n = 'eat';
console.log(obj);
console.log(obj2);
总结:
- 不能识别BigInt类型
- 不能拷贝undefined、Symbol、function类型的值
- 不能处理循环引用
structuredClone()
这种复制方式为深拷贝,拷贝后的新对象与原对象互相独立,所以修改原对象不会影响新对象
ini
const user = {
name: {
firstName: 'tt',
lastName: 'l'
},
age: 18
}
const newUser = structuredClone(user);
user.name.firstName = 'xp';
console.log(newUser);
可能会在终端报错,和上面一样,node版本的问题
手写深拷贝
- 通过for in遍历数组
- 当遍历到数组时采用递归再次遍历知道不为数组再复制
- 否则直接复制
ini
const obj = {
name: {
firstName: 'tt',
lastName: 'l'
},
age: 18
}
function deep(obj) {
let newObj = {};
for (let key in obj) {
if(obj.hasOwnProperty(key)) {
if (obj[key] instanceof Object) { // obj[key] 是不是对象
newObj[key] = deep(obj[key]);
}else{
newObj[key] = obj[key];
}
}
}
return newObj;
}
let obj2 = deep(obj);
obj.name.firstName = 'xp';
console.log(obj);
console.log(obj2);
实现原理
- 借助for in 遍历原对象,将原对象的属性值增加到新对象中
- 因为for in 会遍历到对象隐式具有的属性,通常要使用 obj.hasOwnProperty(key)来判断要拷贝的属性是不是对象显示具有的
- 如何遍历到的属性值为原始值类型,直接往新对象中赋值,如果是 引用类型,递归创建新的子对象
结语
浅拷贝和深拷贝的概念、常用方法和手写方法都理解了吗?记得来回复习哦,温故而知新,可以为师矣~