数据类型
基础数据
1. 基础数据类型在内存中,以键值对的形式存储在栈内存中;
引用类型
1. 引用数据类型的存储,如下图所示,栈内存中存放的是变量名及对应值的内存引用地址;
浅拷贝/深拷贝
浅拷贝: 指的是直接将a变量赋值给b变量,当a为基本数据类型时,a,b的值相互独立,即基本数据类型的拷贝;当a变量为引用类型时,浅拷贝获得的b变量对应的内存引用地址与a相同,因此修改任一变量都会影响另一个变量;
深拷贝: 特指引用类型数据的拷贝赋值,对应引用类型数据的拷贝,即引用类型变量a赋值给变量b,实现的是值的完全拷贝,深拷贝后的引用变量a和引用变量b,相互独立;
深拷贝方法:
1. JSON类型强制转换
var a = {name:'ming',age:18};
var b = JSON.parse(JSON.stringify(a))
2. lodash库方法常用拷贝
import _ from 'lodash';
var a = {name:'ming',age:18};
// 浅拷贝,只针对指针的拷贝,两个变量指针指向同一个内存地址,互相影响
var b = _.clone(a);
// 深拷贝,内存地址(指针)及值的完全拷贝,两个变量相互独立;
var b = _.cloneDeep(a);
/*浅对比拷贝*/
//_.defaults(object, [sources]),object:目标对象;[source]:涞源对象
// 一旦设置了相同属性的值,后续的将被忽略掉。
_.defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 });
// => { 'a': 1, 'b': 2 }
/*深对比拷贝*/
//_.defaultsDeep(object, [sources]),递归分配默认属性,如果目标对象没有该属性,则添加
_.defaultsDeep({ 'a': { 'b': 2 } }, { 'a': { 'b': 1, 'c': 3 } });
// => { 'a': { 'b': 2, 'c': 3 } }
/*注意*/
原生Object.assign(object,[source]) 无法实现深拷贝
Object.assign==> 合并多个JavaScript对象(第一个参数是目标对象,后面的都是源对象,assign方法将多个原对象的属性和方法都合并到了目标对象上面,如果在这个过程中出现同名的属性(方法),后合并的属性(方法)会覆盖之前的同名属性(方法)
3. 递归实现深拷贝
var obj = { //原数据,包含字符串、对象、函数、数组等不同的类型
name:"test",
main:{
a:1,
b:2
},
fn:function(){
},
friends:[1,2,3,[22,33]]
}
function copy(obj){
let newobj = null; //声明一个变量用来储存拷贝之后的内容
//判断数据类型是否是复杂类型,如果是则调用自己,再次循环,如果不是,直接赋值即可,
//由于null不可以循环但类型又是object,所以这个需要对null进行判断
if(typeof(obj) == 'object' && obj !== null){
//声明一个变量用以储存拷贝出来的值,根据参数的具体数据类型声明不同的类型来储存
newobj = obj instanceof Array? [] : {};
//循环obj 中的每一项,如果里面还有复杂数据类型,则直接利用递归再次调用copy函数
for(var i in obj){
newobj[i] = copy(obj[i])
}
}else{
newobj = obj
}
return newobj; //函数必须有返回值,否则结构为undefined
}
var obj2 = copy(obj)
obj2.name = '修改成功'
obj2.main.a = 100
console.log(obj,obj2)
特点
- 基本类型值在内存中占据固定大小的空间,因此被保存在栈内存中;
- 从一个变量向另一个变量复制基本类型的值,会创建这个值的一个副本;
- 引用类型的值是对象,保存在堆内存中;
- 包含引用类型值的变量实际上包含的并不是对象本身,而是一个指向该对象的指针;
- 从一个变量向另一个变量复制引用类型的值,复制的其实是指针,因此两个变量最终都指向同一个对象;
垃圾回收
垃圾回收:只针对引用类型数据的回收,基本数据类型存放在栈内存中,使用之后会自动回收;
1. 标记清除
垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。
然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。
而在此之后再被加上标记的变量将被视为准备删除的变量,
原因是环境中的变量已经无法访问到这些变量了。
理解:在内存中(堆内存)假设所有变量(堆中存储的)都需要清除;
然后遍历执行环境中所有的变量以及变量引用的变量(执行环境中的变量,在栈内存中),
然后删除这些“有用”的变量身上的标记(删除执行环境中变量对应的存储变量/值);
剩余,在堆内存中还有标记的内存变量,此时已经没有执行环境变量(栈中)可以访问到这些变量(堆内存中),因此这些无法被访问的变量就可以被清除了;
例比:标记有用对象的过程其实就是可达测试的过程。你可以想象整个内存是一个大海,每个对象都是一个岛屿,相互之间用大桥连接,现在要找出与大陆相连的岛屿,那么就从大陆出发,依次标记所能达到的每个岛屿,最后没有被标记到的岛屿就是孤岛,可以当垃圾清除掉。即使两个孤岛之间相互有桥连接也没用,因为与大陆不通啊。这个大陆就是应用程序,或者说是应用程序里的根对象。每次遍历的时候都是从这个根对象出发的。
参考: https://segmentfault.com/q/1010000018711986?utm_source=sf-similar-question
2. 引用次数
其含义是跟踪记录每个值被引用的次数,如果声明了一个变量a,并将引用类型值赋值给a,则该引用类型值的引用次数+1;反之,如果引用该值的变量a重新取了另一个引用类型值,则该值的引用次数-1;当引用次数为0时,此时垃圾收集器任务该引用类型值‘无用’,即可回收;
缺点:循环引用。循环引用指的是对象A 中包含一个指向对象B 的指针,而对象B 中也包含一个指向对象A 的引用;
function problem(){
var objectA = new Object();
var objectB = new Object();
objectA.someOtherObject = objectB;
objectB.anotherObject = objectA;
}
如图,objectA和objectB对应的引用类型值(即0x123和0x888对应的存储值);产生循环引用后,0x123分别被objectA,objectB.anotherObject;0x888分别被objectB,objectA.someOtherObject 引用2次,当执行环境执行完毕后,即objectA和objectB被释放掉,但是无法清除堆内存中的引用,因此两个引用类型的值就不会“清零”,因此标记清除策略更常用;