定义
在所有编程语言中都存在内存空间,例如C语言这样的底层语言一般都有底层的内存管理接口,比如malloc()和free()。所以在javascript创建变量(对象,字符串等)时分配内存。并且在不再使用它们时“自动”释放。后一个过程成为垃圾回收。这一套流程就是javascript的内存管理过程
内存生命周期
无论什么编程语言,内存生命周期基本是一致的:
1、分配你所需要的内存
2、使用分配到的内存(读、写)
3、不需要时将其释放/归还
所有语言第二部分都是明确的。第一和第三部分在底层语言中是明确的,但在像JavaScript这些高级语言中,大部分都是隐含的。
内存空间在JavaScript中的作用
在过去的很长一段时间内我对于内存空间是没什么概念的,可当我开始关注JavaScript的基础知识时,却发现由于缺乏对于它们的模糊认知,导致很多东西我都不理解,知其然而不知其所以然。例如:引用数据类型和引用传递到底是咋回事?比如深拷贝和浅拷贝有什么不同?还有闭包和原型等……
因此,想要对JS的理解更加深刻,就必须对内存空间又一个清晰的认知
在学习内存空间之前,我们需要了解三种数据结构,它们分别是堆(heap)、栈(statck)与队列(queue)
栈数据结构**
与C/C++不同,JavaScript中并没有严格意义上区分栈内存和堆内存。因此我们可以简单粗暴地理解为JavaScript的所有数据都保存在堆内存中。但是在某些场景,我们仍然需要基于堆栈数据结构的思维来实现一些功能,比如JavaScript的执行上下文。执行上下文的执行顺序借用了栈数据结构的存取方式。(也就是后面我们经常会提到的函数调用栈)。因此理解栈数据结构的原理和特点十分重要。
要理解栈的存取方式,我们可以通过类比乒乓球盒子来分析。如下图所示:

乒乓球盒子与栈类型对比
如上图所示,栈数据结构类似乒乓球盒子,出口和入口都是同一个,所以存取数据的方式是先进后出,后进先出
**
堆数据结构
堆数据结构是一种树状结构,它的存取数据方式,与书架管理书的方式类似。
书虽然也是整齐的摆在书架上,但是我们只要知道书的名字,我们就可以很方便的取出我们想要的书,而不用像从乒乓球盒子里取乒乓球一样,只有一个口子,只能按顺序把后放进去的球先拿出来,才能取到想要的球。其实堆数据结构就是一种类似key-value对的形式存储的,所以它是无序的。我们不需要关心存储顺序,而只要知道待取值的key就行了
队列
在JavaScript中,理解队列数据结构的主要目的主要是为了清晰的明白事件循环(Event Loop)的机制到底是怎么回事。在后续的章节中我会详细分析事件循环机制。
队列是一种先进先出的数据结构,正如排队过安检一样,排在队伍前面的人一定是最先过检的人。用以下的图示可以清楚地理解队列的原理

变量对象与基础数据类型
JavaScript的执行上下文创建之后,会创建一个叫做变量对象的特殊对象,JavaScript的基础数据类型往往都会保存在变量对象中。
:::info 严格意义上来说,变量对象也是存放在堆内存当中,但是由于变量对象的特殊职能,我们在理解时仍然需要将其与堆内存区分开来 :::
基础数据类型都是一些简单的数据段,JavaScript中有5中基础数据类型,分别是undefined``、``Boolean、Null、String、Number(ES6中新增的基本数据类型Symbol,我们暂时不需要考虑它)。基础数据类型都是按值访问,因为我们可以直接操作保存在变量中的实际的值。
引用数据类型与堆内存
与其他语言不同,JS的引用数据类型,比如数组Array,它们的值的大小是不固定的。引用数据类型的值是保存在堆内存中的对象。JavaScript不允许直接访问堆内存中位置,因此我们不能直接操作对象的堆内存空间。在操作对象时,实际上是在操作对象的引用而不是实际的对象。因此,引用类型的值都是按引用访问的。这里的引用,我们可以理解为保存在变量对象中的一个地址,该地址与堆内存的实际值相关联。
为了更好的搞懂变量对象与堆内存,我们可以结合以下例子与图解进行理解。
var a1 = 0; // 变量对象var a2 = 'this is String'; // 变量对象var a3 = null; // 变量对象var b = { m: 20 }; // 变量b存在变量对象中,{ m: 20 }作为对象存在于堆内存中var c = { 1, 2, 3 }; // 变量c存在于变量对象中,[1,2,3]作为对象存在于堆内存中

因此,当我们要访问堆内存中的引用数据类型时,实际上我们首先是从变量对象中获取了该对象的地址引用(或者地址指针),然后再从堆内存中取得我们需要的数据。
理解JS的内存空间,我们就可以借助内存空间的特性来验证一下引用类型的特点了。
在前端面试中,我们经常会遇到这样一个类似的题目
// demo01var a = 20;var b = a;b = 30;// 这时a的值为多少
// demo02var m = { a: 10, b: 20 };var n = m;n.a = 15//这时m.a的值又是多少
在变量对象中的数据发生复制行为时,系统会自动为新的变量分配一个新值。var b = a执行以后,a和b的值虽然都等于20,但是他们其实已经是相互独立互不影响的值了。具体如图。所以我们修改了b的值以后,a的值并不会发生变化。

在demo02中,我们通过var n = m执行一次复制引用类型的操作,引用类型的复制同样也会为新的变量自动分配一个新的值保存在变量对象中,但不同的是,这个新值,仅仅只是引用类型的一个地址指针。当地址指针相同时,尽管它们相互独立,但是在变量对象中访问到的具体对象实际是同一个。因此当我改变n时,m也发生了变化。这就是引用类型的特效。如图所示:

通过内存的角度来理解。是不是感觉要轻松很多?除此之外,我们还可以以此为基础,一步一步的理解JavaScript的执行上下文,作用域链,闭包,原型链等重要概念。
内存空间管理
因为JavaScript具有自动垃圾回收机制,所以我们在开发时好像不用关心内存的使用情况,内存的分配和回收都完全实现了自动管理。但是仍然会发生内存泄漏的问题,当然关注内存管理不仅仅是为了解决这个问题,它也可以帮助我们很好的优化性能,因此关心内存管理是一件很重要的事。
在文章的开头我就写了内存生命周期,为了便于理解,我们使用一个简单的例子来解释这个周期。
var a = 20; // 在内存中给数值变量分配空间alert(a + 10); // 使用内存a = null; // 使用完毕以后,释放内存
第一步和第二步都很好理解,JavaScript在定义变量的时候就完成了内存分配。第三步释放内存空间则是我们需要重点理解的一个点。
JavaScript的自动回收机制就是垃圾回收,垃圾回收主要依赖于引用的概念。在内存管理的环境中,一个对象如果有访问另一个对象的权限,叫做一个对象引用另一个对象。例如,一个JavaScript对象具有对它原型的引用,和对它属性的引用
在这里,‘对象’的概念不仅特指JavaScript对象,还包括函数作用域。而JavaScript的垃圾回收机制就是引用计数垃圾回收。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用它”,如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
需要注意的是:
:::info 在局部作用域中,当函数执行完毕,局部变量也就没有存在的必要了,因此垃圾收集器很容易做出判断并回收。但是全局变量什么时候需要自动释放内存空间则很难判断,因此在我们的开发中,应该尽量避免使用全局变量。 :::
更多关于垃圾回收机制的内容请查看红宝书
本文章内容引用自:MDN内存管理、这波能反杀的内存空间详细图解
