详解 JavaScript 内存空间
因为 JavaScript 有垃圾自动回收机制,所以对于前端开发人员来说,内存空间并不是一个经常被提及的概念,所以很容易被大家忽视。特别是很多非计算机专业的人员在进入前端行业之后,通常对内存空间的认知比较模糊,甚至一无所知。但是内存空间其实是真正的基础,这是我们进一步理解闭包等重要概念的理论基础,所以非常有必要对其进行了解。
基础数据类型与变量对象
最新的 ECMAScript 标准号定义了 7 种数据类型,其中包括六种及基础数据类型与一种引用数据类型,其中基础数据类型如下表所示。
由于目前常用的浏览器版本还不支持 Symbol,而且通过babel
编译之后的代码量过大,因此在实践中建议暂时不要使用 Symbol。
function fn() {
let a1 = 10;
let a2 = "hello";
let a3 = null;
}
在上述代码示例中,我们首先需要思考的是,当运行函数fn
时,它其中的变量a1
、a2
、a3
都保存在什么地方?
函数运行时,会创建一个执行环境,这个执行环境叫做执行上下文。在执行上下文中,会创建一个叫做变量对象的特殊对象。基础数据类型往往都保存在变量对象之中,如下图所示。
变量对象也存在于堆内存中,但是由于变量对象有特殊职能,因此在理解时,建议仍然将其与堆内存空间区分开来。
引用数据类型与堆内存空间
引用数据类型的值是保存在堆内存空间中的对象。在 JavaScript 中,不允许直接访问堆内存空间中的数据,因此不能直接操作对象那个的堆内存空间。在操作对象时,实际上是在操作对象的引用而不是实际的对象。因此,引用数据类型都是按引用访问的。这里的引用,可以理解为保存在变量对象中的一个地址,该地址与堆内存中的对象相关联。为了更好地理解变量对象与堆内存,下面用一个示例与图解配合讲解。
function foo() {
let a1 = 10;
let a2 = "hello";
let a3 = null;
let b = {
m: 20,
};
let c = [1, 2, 3];
}
如下图所示,当我们想要访问堆内存空间中的数据类型时,实际上是通过一个引用(地址指针)来访问的。
在前端面试题中,我们常常会遇到这样一个类似的题目。
let a = 20;
let b = a;
b = 30;
// 这时 a 的值是多少?
let m = {
a: 10,
b: 20,
};
let n = m;
n.a = 15;
// 这时 m.a 的值是多少?
在上述第一段代码中,基础数据类型发生了一次复制行为。在第二段代码中,引用数据类型发生了一次复制行为。
当变量对象中的数据发生复制行为时,新的变量会被分配到一个新的值。在第一段代码中,通过let b=a
发生复制之后,虽然a
与b
的值都等于20
,但事它们其实已经是相互独立互不影响的值了。因此当我们们修改了b
的值以后,a
的值并不会发生变化,具体如下图所示。
在第二段代码中,通过let n = m
发生了一次复制行为。引用类型的复制同样会为新的变量自动分配一个新的值并保存在变量对象之中。但不同的是,这个新的值,仅仅只是引用类型的一个地址指针。当地址指针相同时,尽管它们相互独立,但是它们指向的具体对象实际上是同一个。因此,当修改n
时,m
也会发生变化,这就是引用类型的特性,具体如下图所示。
内存空间管理
因为自动垃圾回收机制的存在,使得我们在开发时好像并不用那么关心内存的使用问题,内存的分配和回收完全实现了自动管理。但是根据经验来看,了解内存机制有助于自己清晰地认识到自己写的代码在执行过程中都发生了什么,从而写出性能较为优秀的代码。因此在成为更好的前端开发者的道路上,关心内存空间管理是一件非常重要的事情。
下面通过一个非常简单的例子来了解内存空间的使用过程。
let a = 20;
alert(a + 100);
a = null;
上面的三条语句,分别对应:分配内存、使用分配到的内存以及不需要时释放内存。
分配内存与使用内存都比较好理解,我们需要重点理解的是第三个过程。这里涉及到 JavaScript 垃圾回收机制的实现原理。JavaScript 的垃圾回收实现主要依靠“引用”这一概念。当一块内存中的数据能够被访问时,垃圾回收器就认为“该数据能够被获得”。不能够被获得的数据,就会被打上标记,并回收内存空间。这种方式叫做标记——清除算法。
这个算法会设置一个全局对象,并定期地从全局对象开始查找、垃圾回收器会找到所有可以获得与不能够被获得的数据。因此在上面的这个例子中,当我们将a
设置为null
时,那么刚开始分配的20
,就无法被访问到了,而是很快被自动回收。
在局部作用域中,当函数执行完毕后,局部变量也就没有存在的必要了,因此垃圾收集器很容易做出判断并回收。但是在全剧终,变量什么时候需要自动释放内存空间则很难判断,因此我们在开发时,应当尽量避免使用全局变量。如果使用了全局变量,则建议不再使用它时,通过a=null
这样的方式释放引用,从而确保能够及时回收内存空间。