又有一段时间没有总结自己的所学所用了,懒惰无时无刻不在,还是要强迫自己做点什么来提醒自己掌握了哪些。今天回忆了一下当初刚接触javascript时,所学到的关于堆和栈的概念,那时候是一头雾水,从文字表面根本发现不了其含义,更不知道堆栈的区别是什么,但是这个知识点其实在我们编写js代码时,处处都在用到。今晚趁着睡意未到,来总结回忆一波,各位兄台可以多多指教,小弟感激不尽。
一: 数据类型
为了更好容易的理解堆和栈,首先来复习一下js中的数据类型。
在js中数据类型主要分为以下两大类:
- 基本类型:String,Number,Boolean,Null,Undefined,这5种基本数据类型它们是直接按值存放的,所以可以直接访问。
- 引用类型:Function,Array,Object,当我们需要访问这三种引用类型的值时,首先得从栈中获得该对象的地址指针,然后再从堆内存中取得所需的数据。
现在咱再来瞅一瞅概念,什么是堆,什么是栈!
二: 什么是堆什么是栈
其实了解过一些数据结构的兄台应该都对堆和栈有一定的认识,堆栈不仅仅只有js中才有这个概念,不过这片文章只针对js来进行描述。
- 栈(stack): 由操作系统自动分配内存空间,自动释放,存储的是基础变量以及一些对象的引用变量,占据固定大小的空间。
- 堆(heap):由操作系统动态分配的内存,大小不定也不会自动释放,一般由程序员分配释放,也可由垃圾回收机制回收。
三: 堆和栈的区别
1、栈:基础变量的值是存储在栈中,而引用变量存储在栈中的是指向堆中的数组或者对象的地址,这就是为何修改引用类型总会影响到其他指向这个地址的引用变量。
优点:相比于堆来说存取速度会快,并且栈内存中的数据是可以共享的,例如同时声明了var a = 1和var b = 1,会先处理a,然后在栈中查找有没有值为1的地址,如果没有就开辟一个值为1的地址,然后a指向这个地址,当处理b时,因为值为1的地址已经开辟好了,所以b也会同样指向同一个地址。
缺点:相比于堆来说的缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。
2、堆:堆内存中的对象不会随方法的结束而销毁,就算方法结束了,这个对象也可能会被其他引用变量所引用(参数传递)。创建对象是为了反复利用(因为对象的创建成本通常较大),这个对象将被保存到运行时数据区(也就是堆内存)。只有当一个对象没有任何引用变量引用它时,系统的垃圾回收机制才会在核实的时候回收它。
关于堆的优缺点大家只要看一下栈的优缺点相信就会有自己的判断了。
四: 传值与传址
基本类型与引用类型最大的区别实际就是传值与传址的区别。瞅一瞅这个例子:
var a = [1,2,3,4,5];
var b = a;
var c = a[0];
alert(b);//[1,2,3,4,5]
alert(c);//1
//改变数值
b[4] = 6;
c = 7;
alert(a[4]);//6
alert(a[0]);//1
以上的例子中可以看出改变b的值时,a的数组也发生了改变;但是改变c的值时,a的数组毫无变化,这个就是传值和传址的区别。
因为a是数组,属于引用类型,所以它赋予给b的时候传的是栈中的地址(指向a这个数组在堆内存中的地址),而不是直接传的堆内存中的对象,当b发生改变时,也就会根据地址回到a在堆内存中的位置进行修改;而c仅仅是从a在堆内存中获取的一个数据值,并保存在栈中,所以c是直接在栈中修改,并且不能指向a在堆内存中的地址。
五: 深浅拷贝
1、浅拷贝:直接来例子,看完例子在讲解。
var a = {
key1:"11111"
}
function Copy(p) {
var c = {};
for (var i in p) {
c[i] = p[i];
}
return c;
}
a.key2 = ['小辉','小辉'];
var b = Copy(a);
b.key3 = '33333';
alert(b.key1); //11111
alert(b.key3); //33333
alert(a.key3); //undefined
b.key2.push("大辉");
alert(b.key2); //小辉,小辉,大辉
alert(a.key2); //小辉,小辉,大辉
从以上例子可以看出浅拷贝其实可以理解成只拷贝属性的第一层,在复制属性的基本类型时,直接用等号完成,如果遇到属性还是引用类型就会先将堆内存中的地址复制过去。
a对象中key1属性是字符串,key2属性是数组。a拷贝到b,key1和key2属性均顺利拷贝。给b对象新增一个字符串类型的属性key3时,b能正常修改,而a中无定义。说明子对象的key3(基本类型)并没有关联到父对象中,所以undefined。
但是,若修改的属性变为对象或数组时,那么父子对象之间就会发生关联,因为指向的堆内存中的地址是同一个。从以上弹出结果可知,我对b对象进行修改,a、b的key2属性值(数组)均发生了改变。其在内存的状态,可以用下图来表示。
2、深拷贝:如果我们希望父子对象在任何时候都不产生关联,那我们就得用深度拷贝了,其实就是用递归的方法将引用类型的属性都赋值给子对象就可以了。瞅例子:
function Copy(p, c) {
var c = c || {};
for (var i in p) {
if (typeof p[i] === 'object') {
c[i] = (p[i].constructor === Array) ? [] : {};
Copy(p[i], c[i]);
} else {
c[i] = p[i];
}
}
return c;
}
a.key2 = ['小辉','小辉'];
var b={};
b = Copy(a,b);
b.key2.push("大辉");
alert(b.key2); //小辉,小辉,大辉
alert(a.key2); //小辉,小辉
由上可知,修改b的key2数组时,没有使a父对象中的key2数组新增一个值,即子对象没有影响到父对象a中的key2。简单的理解就是此时此刻,b中key2的数组和a中key2的数组虽然赋值相同,但是指向堆内存中的地址已经不是同一个了,所以不管修改哪一个key2都不会影响到另一个。其存储模式大致如下:
六: 堆和栈的溢出
如果想要堆溢出,比较简单,可以循环创建对象或大的对象;
如果想要栈溢出,可以递归调用方法,这样随着栈深度的增加,JVM (虚拟机)维持着一条长长的方法调用轨迹,直到内存不够分配,产生栈溢出。
关于堆和栈的概念就讲到这里咯,有任何问题或者不同见解的,再或者遗漏哪些重要知识点的,都欢迎各位大牛们留言解答,小弟感激不尽~~