一、浅拷贝和深拷贝的区别
1.1 什么是拷贝?
答:ctrl + c
ECMASciprt包括两个不同类型的值:基本数据类型和引用数据类型。
-
基本数据类型:(值传递)(存储在栈内存中)
常见的基本数据类型:Number,String, Boolean, Null, Undefined。
不常见的基本数据类型:BigInt,Symbol。
-
引用数据类型:Object,Array,Date,function,RegExp...。(地址传递)(存储在堆内存中)
总结区别:
内存分配的区别
-
基本数据类型:
存储在栈内存中的简单数据段,也就是说,它们的值直接储存在变量访问的位置。这是因为:这些原始类型占据的空间是固定的,所以可将他们存储在较小的内存区域 - 栈中。这样存储便于迅速查询变量的值。
-
引用数据类型:
储存在堆内存中的对象,也就是说,储存在变量处的值是一个指针(point),指向存储对象的内存地址。这是因为:引用值的大小会改变,所以不能把它放在栈中,否则会降低变量查询的速度。相反,放在变量的栈空间中的值是该对象存储在堆中的地址。地址的大小是固定的,所以把它存储在栈中对变量性能无任何负面影响。
访问机制区别
-
基本数据类型:
可以直接访问
-
引用数据类型:
不允许直接访问保存在堆内存中的对象的,所以在访问一个对象时,首先得到的是这个对象在堆内存的地址,然后再按照这个地址去获取这个对象中的值,这就是 按引用访问
复制变量时的区别
-
基本数据类型:
在将一个保存着原始值的变量复制给另一个变量时,会将原始值的副本赋值给新变量,此后这两个变量是完全独立的,他们只是拥有相同的value而已。
-
引用数据类型:
在将一个保存着原始值的变量赋值给另一个变量时,会把这个内存地址赋值给新变量。也就是说这两个变量都指向了内存中的同一地址,它们中任何一个做出的改变都会反映到另一个身上。(这里要理解一点的就是,复制对象时并不会在堆内存中新生成一个一模一样的对象,只是对了一个保存指向这个对象指针的变量而已)。
-
浅拷贝实现方式:
常见对象浅拷贝的方式:
Object.assign()、扩展运算符
//方式一、Object.assign()
const obj1 = { name: '张三', info: { age: 18 } } ;
const obj2 = Object.assign({}, obj1);
obj2.name = '李四' ;
obj2.info.age = 19 ;
console.log(obj1) // { name: '张三', info: { age: 19} }
console.log(obj2) // { name: '李四', info: { age: 19 }
//方式二、扩展运算符
const obj2 = { ...obj1 }
obj2.name = '李四' ;
obj2.info.age = 19 ;
console.log(obj1) // { name: '张三', info: { age: 19} }
console.log(obj2) // { name: '李四', info: { age: 19 }
上述例子可以看出当拷贝后的对象obj2.info.age发生改变obj1.info.age也发生了改变,因为info对象拷贝的是源对象指针。
-
深拷贝实现方式:
常见深拷贝的方式:
JSON.parse(JSON.stringify(data))配合使用
//方式一、JSON.parse(JSON.stringify(data))
const obj1 = { name: '张三', info: { age: 18 } } ;
const obj2 = JSON.parse(JSON.stringify(obj1)) ;
obj2.name = '李四' ;
obj2.info.age = 19 ;
console.log(obj1) // { name: '张三', info: { age: 18 } }
console.log(obj2) // { name: '李四', info: { age: 19 } }
//方式一弊端`
// undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略。
// Date 日期调用了toJSON()将其转换为了string 字符串(Date.toISOString()),因此会被当做字符串处理。
// NaN 和 Infinity 格式的数值及 null 都会被当做 null。
// 其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性。
// 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
//方式二、函数封装
let deepCopy = function (obj) {
// 只拷贝对象
if (typeof obj !== 'object') return;
// 根据obj的类型判断是新建一个数组还是对象
let newObj = obj instanceof Array ? [] : {};
// 遍历obj,并且判断是obj的属性才拷贝
for (let key in obj) { if (obj.hasOwnProperty(key)) {
// 如果obj的子属性是对象,则进行递归操作,否则直接赋值
newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
}
}
return newObj;
}
方式三、
//使用第三方库`Lodash`中的`_.cloneDeep()`。
Lodash 中文文档: https://www.lodashjs.com/)https://www.lodashjs.com/docs/lodash.cloneDeep#_clonedeepvalue
知识拓展:
变量存储 (栈 & 堆)
-
栈
栈是内存中一块用于存储局部变量和函数参数的线性结构,遵循着先进后出的原则。数据只能顺序的入栈,顺序的出栈。当然,栈只是内存中一片连续区域一种形式化的描述,数据入栈和出栈的操作仅仅是栈指针在内存地址上的上下移动而已。
较为经典的就是乒乓球盒结构,先放进去的乒乓球只能最后取出来。
特点:轻量,不需要手动管理,函数调用时创建,调用结束则消失(出栈)。
-
堆
是堆内存的简称,堆是动态分配内存,内存大小不固定,也不会自动释放,堆数据结构是一种无序的树状结构,同时它还满足key-value键值对的存储方式;我们只用知道key名,就能通过key查找到对应的value。
比较经典的就是书架存书的例子,我们知道书名,就可以找到对应的书籍;
特点:速度稍慢、容量比较大;
内存分配&垃圾回收
-
内存分配
栈内存:线性有序存储,容量小,系统分配效率高。
堆内存:首先要在堆内存新分配存储区域,之后又要把指针存储到栈内存中,效率相对就要低一些了。
-
垃圾回收
栈内存:变量基本上用完就回收了,相比于堆来说存取速度会快,并且栈内存中的数据是可以共享的。
堆内存:堆内存中的对象不会随方法的结束而销毁,就算方法结束了,这个对象也可能会被其他引用变量所引用(参数传递)。创建对象是为了反复利用(因为对象的创建成本通常较大),这个对象将被保存到运行时数据区(也就是堆内存)。只有当一个对象没有任何引用变量引用它时,系统的垃圾回收机制才会在核实的时候回收它。