对于很多没经验的前端开发来说,觉得JS反正有垃圾回收机制,很容易忽视内存空间的管理,这其实是一个大错误。
看了阮一峰老师关于JS内存泄漏的文章,才发现自己以前的代码,存在许多内存泄漏的问题,再者,因为忽略对内存空间的学习,导致后面很多进阶概念很模糊,比如闭包、作用域链,比如深拷贝与浅拷贝的区别等等。
内存空间管理
JavaScript的内存生命周期:
markdown
1. 分配你所需要的内存
2. 使用分配到的内存(读、写)
3. 不需要时将其释放、归还
为了便于理解,我们使用一个简单的例子来解释这个周期。
ini
var a = 10; // 在内存中给数值变量分配空间
alert(a + 90); // 使用分配到的内存
a = null; // (null是没有引用指向的)使用完毕之后,释放内存空间
在JS中,每一个数据都需要一个内存空间。内存空间又被分为两种,栈内存(stack) 与堆内存(heap) 。
栈与堆
栈(stack)是有序的,
主要存放一些基本类型的变量和对象的地址(不是对象,是地址),每个区块按照一定次序存放(后进先出)
它们都是直接按值存储在栈中的,每种类型的数据占用的内存空间的大小也是确定的,并由系统自动分配和自动释放。
因此,带来的好处就是内存可以及时得到回收,相对于堆来说,更加容易管理内存空间,且寻址速度也更快。
堆(heap)是没有特别的顺序的
数据可以任意存放,多用于复杂数据类型(引用类型) 分配空间,例如数组对象、object对象。
其实这样说也不太准确.
因为引用类型数据的地址是存储于栈中的,当我们想要访问引用类型的值的时候,需要先从栈中获得想要访问对象的地址.
然后,再通过地址指向找出堆中的所需数据。就好比书架上的书,虽然已经按顺序放好了,但我们只要知道书的名字,就可以对应的取下来。
变量的存放
首先,我们来看一下代码:
ini
//原始类型都放在栈(stack)里
//引用类型都放在堆(heap)里,对象地址放栈(stack)里
var a = 10;
var b = 'lzm';
var c = true;
var d = { n: 22 }; //地址假设为0x0012ff7f,不代表实际地址
var e = { n: 22 }; //重新开辟一段内存空间,地址假设为0x0012ff8c
console.log(e==d); //false
var obj = new Object(); //地址假设为0x0012ff9d
var arr = ['a','b','c']; //地址假设为0x0012ff6e
为什么console.log(e==d)的结果为false?可以用下面的内存图解释:
分析:
变量a,b,c为基本数据类型,它们的值,直接存放在栈中
d,e,obj,arr为复合数据类型,他们的引用变量及地址存储在栈中,指向于存储在堆中的实际对象。
我们是无法直接操纵堆中的数据的,也就是说我们无法直接操纵对象,我们只能通过栈中对对象的引用来操作对象,就像我们通过遥控机操作电视一样,区别在于这台电视本身并没有控制按钮。
变量d,e虽然指向存在堆内存中对象内容的值是相等的,但是它们来自栈内存中变量地址不相同,导致console.log(e==d)的结果为false。
这里就回到了最初的疑问,为什么原始类型值要放在栈中,而引用类型值要放在堆中,为什么要分开放置呢?单列一种内存岂不是更省事吗?那接下来,解释是这样的:
能量是守衡的,无非是时间换空间,空间换时间的问题。(存储数据就是用内存空间与访问时间的置换)
堆比栈大,栈比堆的运算速度快,
对象是一个复杂的结构,并且可以自由扩展,如:数组可以无限扩充,对象可以自由添加属性。将他们放在堆中是为了不影响栈的效率。而是通过引用的方式查找到堆中的实际对象再进行操作。相对于简单数据类型而言,简单数据类型就比较稳定,并且它只占据很小的内存。
不将简单数据类型放在堆是因为通过引用到堆中查找实际对象是要花费时间的,而这个综合成本远大于直接从栈中取得实际值的成本。所以简单数据类型的值直接存放在栈中。
比较抠细节的面试题
下面的几道是关于内存空间的面试题,虽然不是特别的难,但比较扣细节还是老老实实画个内存图再自信的给出正确答案吧。
第一题
css
第一题:
var a = 1
var b = a
b = 2
请问 a 显示是几?
1,在栈内存中的数据发生复制行为时,基础数据类型的赋值动作是直接赋予数值,没有引用关系.
第二题
css
第二题:
var a = {name: 'a'}
var b = a
b = {name: 'b'}
请问现在 a.name 是多少?
"a"。引用类型的赋值是赋值引用地址,系统在堆内存中会自动分配内存给对象{name:'b'},同时栈内存中变量b的指向地址也随之变化,变量a不受影响。
第三题
css
第三题:
var a = {name: 'a'}
var b = a
b.name = 'b'
请问现在 a.name 是多少?
"b"。a,b的引用地址是相同的,当地址指针相同时,尽管他们相互独立,但是在堆内存中访问到的具体对象实际上是同一个,因此b.name ='b'使堆内存中对象的value值变化,a.name的值也随之变化。
第四题
css
第四题:
var a = {name: 'a'}
var b = a
b = null
请问现在 a 是什么?
{name: "a"}。因为null为基本类型,存在栈内存当中。因此栈内存中的变量b由之前指向对象的一个地址转变为null,变量a的地址还是指向原先的对象。
第五题
css
第五题:
var a = {n: 1};
var b = a;
a.x = a = {n: 2};
a.x // 这时 a.x 的值是多少
b.x // 这时 b.x 的值是多少
答案是:a.x --> undefined;b.x --> {n: 2}。这道题的关键在于:
- 1、优先级。
.
的优先级高于=
,所以先执行a.x
,堆内存中的{n: 1}
就会变成{n: 1, x: undefined}
,改变之后相应的b.x
也变化了,因为指向的是同一个对象。 - 2、赋值操作是
从右到左
,所以先执行a = {n: 2}
,a
的引用就被改变了,然后这个返回值又赋值给了a.x
,需要注意 的是这时候a.x
是第一步中的{n: 1, x: undefined}
那个对象,其实就是b.x
,相当于b.x = {n: 2}