很浅显的讲解一下堆和栈
含义及对比
如果原生JavaScript掌握不扎实,我先讲解一下(建议读一下《JavaScript权威指南》,淘宝团队翻译的)。
JavaScript基本数据类型
- 基本数据类型:String,Boolean,Number,Undefined,Null;
- 引用数据类型:Object(Array,Date,RegExp,Function);
- 基本数据类型和引用数据类型的区别:
- 保存位置不同:基本数据类型保存在栈内存中,引用数据类型保存在堆内存中,然后在栈内存中保存了一个对堆内存中实际对象的引用,即数据在堆内存中的地址,JS对引用数据类型的操作都是操作对象的引用而不是实际的对象,如果obj1拷贝了obj2,那么这两个引用数据类型就指向了同一个堆内存对象,具体操作是obj1将栈内存的引用地址复制了一份给obj2,因而它们共同指向了一个堆内存对象;
为什么基本数据类型保存在栈中,而引用数据类型保存在堆中?
1)堆比栈大,栈比堆速度快;
2)基本数据类型比较稳定,而且相对来说占用的内存小;
3)引用数据类型大小是动态的,而且是无限的,引用值的大小会改变,不能把它放在栈中,否则会降低变量查找的速度,因此放在变量栈空间的值是该对象存储在堆中的地址,地址的大小是固定的,所以把它存储在栈中对变量性能无任何负面影响;
4)堆内存是无序存储,可以根据引用直接获取; - 基本数据类型使用typeof可以返回其基本数据类型,但是NULL类型会返回object,因此null值表示一个空对象指针;引用数据类型使用typeof会返回object,此时需要使用instanceof来检测引用数据类型;
- ES6新增数据类型:Map,Set,Generator,Symbol
关于更详细的数据类型介绍,请看 JS深拷贝和浅拷贝的实现,现在我们主要讲解一下堆和栈。
堆和栈
堆(heap):用于复杂数据类型(引用数据类型)分配空间,例如数组对象、object对象;它是在程序运行时,而不是在程序编译时,申请某个大小的内存空间。即动态分配内存,对其访问和对一般内存的访问没有区别。对于堆,我们可以随心所欲的进行增加变量和删除变量,不用遵循次序。
栈(stack):中主要存放一些基本数据类型的变量和对象的引用,(包含池,池存放常量),其优势是存取速度比堆要快,并且栈内的数据可以共享,但缺点是存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。先进后出型号,就像一个桶,后进去的先出来,它下面本来有的东西要等其他出来之后才能出来
数据类型访问和复制(堆内存和栈内存对数据类型的存放)
基本数据类型:基本数据类型值指保存在栈内存中的简单数据段。访问方式是按值访问。
let a = 1;
操作的是变量实际保存的值
a = 2;
基本类型变量的复制:从一个变量向一个变量复制时,会在栈中创建一个新值,然后把值复制到为新变量分配的位置上。
let b = a;
b=2;
引用数据类型复制:引用数据类型值指保存在堆内存中的对象。也就是,变量中保存的实际上的只是一个指针,这个指针指向内存中的另一个位置,该位置保存着对象。访问方式是按引用访问。
let a = new Object()
当操作时,需要先从栈中读取内存地址,然后再延指针找到保存在堆内存中的值再操作。
a.name= 'xz';
引用类型变量的复制:复制的是存储在栈中的指针,将指针复制到栈中未新变量分配的空间中,而这个指针副本和原指针指向存储在堆中的同一个对象;复制操作结束后,两个变量实际上将引用同一个对象。因此,在使用时,改变其中的一个变量的值,将影响另一个变量。
let b = a;
b.sex = 'boy';
这就解释了,为什么 const 定义一个数组,json 可以更改。其实定义的是一个指针。
1.1 扩展: 队列(先进先出(FIFO)的数据结构,这是事件循环(Event Loop)的基础结构)
1.2 经典面试题
var a = 20;
var b = a; // b 为栈类型,相当于新开一块空间然后赋值为 20,不影响原来的a
b = 30;
console.log(a)
console.log(b)
//20 30
var a = { name: '前端开发' }
var b = a;
b.name = '进阶';
console.log(a)
console.log(b)
// { name: '进阶' } {name: '进阶' }
var a = { name: '前端开发' }
var b = a;
a = null; //释放引用
console.log(a)
console.log(b)
// null { name: '前端开发' }
var a = {n: 1};
var b = a;
a.x = a = {n: 2};
// 等价于 b.x = a = {n: 2}; 先是预编译ab同时指向的对象增加x属性,该对象也就是后来都没变的b指向的对象,这里 a= {n: 2};改变了a的指向。
另外扩展下连等算法:
var a=b=1,b为全局变量了
function t(){ var var a=b=1} t()
// b 1 a undefined
console.log(a.x);
console.log(b.x);
// undefined {n:2}
1.3 闭包中的变量并不保存中栈内存中,而是保存在堆内存
中,这也就解释了函数之后之后为什么闭包还能引用到函数内的变量。
function A() {
let a = 1
function B() {
console.log(a)
}
return B
}
2.例子
栈
var a=3;
var b=3;
先处理 var a=3;,首先会在栈中创建一个变量为a引用,然后查找栈中是否有3这个值,如果没有找到,就将3存放进来,然后将a指向3。
接着处理 var b=3;,在创建为b的引用变量后,查找栈中是否有3这个值,因为此时栈中已经存在了3,便将b直接指向3。这样,就出现了a与b同时指向3的情况。
此时,如果再令a=4,那么JavaScript解释引擎会重新搜查栈中是否有4这个值,如果已经有了,则直接将a指向这个地址。没有的话直接压入并指向它。
堆
var fruit_1 = "apple";
var fruit_2 = "orange";
var fruit_3 = "banana";
var oArray = [fruit_1,fruit_2,fruit_3]; (此时相当于新建一个包含三个值的数组,与 fruit等不再相关)
var newArray = oArray;
当创建数组时,就会在堆内存创建一个数组对象,并且在栈内存中创建一个对数组的引用
变量fruit_1、fruit_2、fruit_3为基本数据类型,它们的值直接存放在栈中
newArray、oArray为复合数据类型(引用类型),他们的引用变量存放在栈中, 指向于存放在堆中的实际对象。
比较
var str=new String('abc');
var str='abc';
第一种是用new关键字来新建String对象,对象会存放在堆中,每调用一次就会创建一个新的对象,而不管其字符串值是否相等及是否有必要创建新对象,从而加重了程序的负担。并且堆的访问速度慢。
第二种是在栈中,栈中存放值‘abc’和对值的引用;这种写法在内存中只存在一个值,有利于节省内存空间。同时它可以在一定程度上提高程序的运行速度,因为存储在栈中,其值可以共享,并且由于栈访问更快;
var str1='abc';
var str2='abc';
alert(str1==str2); // true
alert(str1===str2); // true
var str1=new String('abc');
var str2=new String('abc');
alert(str1==str2); // false
alert(str1===str2); // false
3.细节
JavaScript堆不需要程序代码来显示地释放,因为堆是由自动的垃圾回收来负责的,每种浏览器中的JavaScript解释引擎有不同的自动回收方式
一个最基本的原则是:如果栈中不存在对堆中某个对象的引用,那么就认为该对象已经不再需要,在垃圾回收时就会清除该对象占用的内存空间。
因此,在不需要时应该将对对象的引用释放掉(newArray=null;),以利于垃圾回收,这样就可以提高程序的性能。