JavaScript系列之内存空间

JavaScript系列之内存空间

对于很多没经验的前端开发来说,觉得JS反正有垃圾回收机制,很容易忽视内存空间的管理,这其实是一个大错误。

直到最近,看了阮一峰老师关于JS内存泄漏的文章,才发现自己以前写的代码,存在许多内存泄漏的问题,再者,因为忽略对内存空间的学习,导致后面很多进阶概念很模糊,比如闭包、作用域链,比如深拷贝与浅拷贝的区别等等。

这里先介绍内存空间,后续还会通过别的文章来介绍深浅拷贝和内存泄漏。

内存空间管理

JavaScript的内存生命周期:

1. 分配你所需要的内存
2. 使用分配到的内存(读、写)
3. 不需要时将其释放、归还

为了便于理解,我们使用一个简单的例子来解释这个周期。

var a = 10;  // 在内存中给数值变量分配空间
alert(a + 90);  // 使用分配到的内存
a = null; // 使用完毕之后,释放内存空间

在JS中,每一个数据都需要一个内存空间。内存空间又被分为两种,栈内存(stack)堆内存(heap)

与堆

栈(stack)是有序的,主要存放一些基本类型的变量和对象的地址,每个区块按照一定次序存放(后进先出),它们都是直接按值存储在栈中的,每种类型的数据占用的内存空间的大小也是确定的,并由系统自动分配和自动释放。

因此,这样带来的好处就是,内存可以及时得到回收,相对于堆来说,更加容易管理内存空间,且寻址速度也更快。

堆(heap)是没有特别的顺序的,数据可以任意存放,多用于复杂数据类型(引用类型)分配空间,例如数组对象、object对象。

其实这样说也不太准确,因为,引用类型数据的地址是存储于栈中的,当我们想要访问引用类型的值的时候,需要先从栈中获得想要访问对象的地址,然后,再通过地址指向找出堆中的所需数据。就好比书架上的书,虽然已经按顺序放好了,但我们只要知道书的名字,就可以对应的取下来。

变量的存放

首先,我们来看一下代码:

//原始类型都放在栈(stack)里
//引用类型都放在堆(heap)里
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。

这里就回到了最初的疑问,为什么原始类型值要放在栈中,而引用类型值要放在堆中,为什么要分开放置呢?单列一种内存岂不是更省事吗?那接下来,援引这篇文章里边的解释:

记住一句话:能量是守衡的,无非是时间换空间,空间换时间的问题。堆比栈大,栈比堆的运算速度快,对象是一个复杂的结构,并且可以自由扩展,如:数组可以无限扩充,对象可以自由添加属性。将他们放在堆中是为了不影响栈的效率。而是通过引用的方式查找到堆中的实际对象再进行操作。相对于简单数据类型而言,简单数据类型就比较稳定,并且它只占据很小的内存。不将简单数据类型放在堆是因为通过引用到堆中查找实际对象是要花费时间的,而这个综合成本远大于直接从栈中取得实际值的成本。所以简单数据类型的值直接存放在栈中。

比较抠细节的面试题

下面的几道是关于内存空间的面试题,虽然不是特别的难,但比较扣细节你稍不注意就错了,我的建议还是老老实实画个内存图再自信的给出正确答案吧。

第一题
var a = 1 
var b = a 
b = 2 
请问 a 显示是几 
第一题

上图中可以看出,答案为:1。在栈内存中的数据发生复制行为时,系统会自动为新的变量分配一个新值。var b = a执行之后,a与b虽然值都等于1,但是他们其实已经是相互独立互不影响的值了。

第二题
var a = {name: 'a'}
var b = a
b = {name: 'b'}
请问现在 a.name 是多少 
第二题

上图中可以看出,答案为:"a"。因为b ={name:'b'}后相当于重新在堆内存中分配内存给对象{name:'b'},同时栈内存中变量b的指向地址也随之变化,变量a不受影响。

第三题
var a = {name: 'a'} 
var b = a 
b.name = 'b' 
请问现在 a.name 是多少
第三题

上图中可以看出,答案为:"b"。我们通过var b = a执行一次复制引用类型的操作。引用类型的复制同样也会为新的变量自动分配一个新的值保存在栈内存中,但不同的是,这个新的值,仅仅只是引用类型的一个地址指针。当地址指针相同时,尽管他们相互独立,但是在堆内存中访问到的具体对象实际上是同一个,因此b.name ='b'使堆内存中对象的value值变化,a.name的值也随之变化。

第四题
var a = {name: 'a'} 
var b = a 
b = null 
请问现在 a 是什么
第四题

上图中可以看出,答案为:{name: "a"}。因为null为基本类型,存在栈内存当中。因此栈内存中的变量b由之前指向对象的一个地址转变为null,变量a的地址还是指向原先的对象。

第五题
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}

最后来个图总结一下:

以上都是通过内存图来解释关于内存空间的知识,如有不合理的地方,希望指正一下~后续还会增加内存泄漏以及深浅拷贝的文章,敬请期待!

本人Github链接如下,欢迎各位Star

github.com/miqilin21/mi

编辑于 2019-06-14

文章被以下专栏收录