注:本文转载自公众号不知非攻 原文地址javascript
var a = 20;
var b = 'abc';
var c = true;
var d = { m: 20 }
复制代码
由于JavaScript具备自动垃圾回收机制,因此对于前端开发来讲,内存空间并非一个常常被说起的概念,很容易被你们忽视。特别是不少不是计算机专业的朋友在进入到前端以后,会对内存空间的认知比较模糊,甚至有些人干脆就是一无所知。前端
固然也包括我本身。java
在很长一段时间里认为内存空间的概念在JS的学习中并非那么重要。可当我回过头来从新整理JS基础时,发现因为对它的模糊认知,致使了许多知识理解得并不明白。好比最基本的引用数据类型和引用传递究竟是怎么回事儿?浅复制与深复制有什么不一样?闭包究竟是什么?等等。面试
所以,想要对JS的理解更加深入,就必须对内存空间有一个清晰的认知。算法
在学习内存空间以前,咱们须要对三种数据结构有一个清晰的理解。他们分别是堆(heap),栈(stack)与队列(queue)。编程
栈数据结构数组
与C/C++不一样,JavaScript中并无严格意义上区分栈内存与堆内存。所以咱们能够简单粗暴的理解为JavaScript的全部数据都保存在堆内存中。可是在某些场景,咱们仍然须要基于栈数据结构的思惟来实现一些功能,好比JavaScript的执行上下文(关于执行上下文我会在下一篇文章中总结)。执行上下文的执行顺序借用了栈数据结构的存取方式(也就是后面咱们会常常提到的函数调用栈)。所以理解栈数据结构的原理与特色十分重要。数据结构
要简单理解栈的存取方式,咱们能够经过类比乒乓球盒子来分析。以下图左侧。闭包
这种乒乓球的存放方式与栈中存取数据的方式一模一样。处于盒子中最顶层的乒乓球5,它必定是最后被放进去,但能够最早被使用。而咱们想要使用底层的乒乓球1,就必须将上面的4个乒乓球取出来,让乒乓球1处于盒子顶层。这就是栈空间先进后出,后进先出的特色。图中已经详细的代表了栈空间的存储原理。函数
堆数据结构
堆数据结构是一种树状结构。它的存取数据的方式,则与书架与书很是类似。
书虽然也整齐的存放在书架上,可是咱们只要知道书的名字,就能够很方便的取出咱们想要的书,而不用像从乒乓球盒子里取乒乓同样,非得将上面的全部乒乓球拿出来才能取到中间的某一个乒乓球。比如在JSON格式的数据中,咱们存储的key-value
是能够无序的,由于顺序的不一样并不影响咱们的使用,咱们只须要关心书的名字。
队列
在JavaScript中,理解队列数据结构的目的主要是为了清晰的明白事件循环(Event Loop)的机制究竟是怎么回事。在后续的章节中我会详细分析事件循环机制。
队列是一种先进先出(FIFO)的数据结构。正如排队过安检同样,排在队伍前面的人必定是最早过检的人。用如下的图示能够清楚的理解队列的原理。
变量对象与基础数据类型
JavaScript的执行上下文生成以后,会建立一个叫作变量对象的特殊对象(具体会在下一篇文章与执行上下文一块儿总结),JavaScript的基础数据类型每每都会保存在变量对象中。
严格意义上来讲,变量对象也是存放于堆内存中,可是因为变量对象的特殊职能,咱们在理解时仍然须要将其与堆内存区分开来。
基础数据类型都是一些简单的数据段,JavaScript中有5种基础数据类型,分别是Undefined、Null、Boolean、Number、String
。基础数据类型都是按值访问,咱们能够直接操做保存在变量中的实际值。
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的内存空间,咱们就能够借助内存空间的特性来验证一下引用类型的一些特色。
在前端面试中咱们经常会遇到这样一个相似的题目
// demo01.js
var a = 20;
var b = a;
b = 30;
// 这时a的值是多少?
复制代码
// demo02.js
var 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具备自动垃圾收集机制,因此咱们在开发时好像不用关心内存的使用问题,内存的分配与回收都彻底实现了自动管理。可是根据以往的开发经验,了解内存机制有助于本身清晰的认识到本身写的代码在执行过程当中发生过什么,从而写出性能更加优秀的代码。
关心内存是一件很是重要的事情。
JavaScript的内存生命周期
为了便于理解,咱们使用一个简单的例子来解释这个周期。
var a = 20; // 在内存中给数值变量分配空间
alert(a + 100); // 使用内存
a = null; // 使用完毕以后,释放内存空间
复制代码
第一步和第二步咱们都很好理解,JavaScript在定义变量时就完成了内存分配。第三步释放内存空间则是咱们须要重点理解的一个点。
JavaScript有自动垃圾收集机制,那么这个自动垃圾收集机制的原理是什么呢?其实很简单,就是找出那些再也不继续使用的值,而后释放其占用的内存。垃圾收集器会每隔固定的时间段就执行一次释放操做。
在JavaScript中,最经常使用的是经过标记清除的算法来找到哪些对象是再也不继续使用的,所以a = null
其实仅仅只是作了一个释放引用的操做,让 a 本来对应的值失去引用,脱离执行环境,这个值会在下一次垃圾收集器执行操做时被找到并释放。而在适当的时候解除引用,是为页面得到更好性能的一个重要方式。
须要注意的是,在局部做用域中,当函数执行完毕,局部变量也就没有存在的必要了,所以垃圾收集器很容易作出判断并回收。可是全局变量何时须要释放内存空间则很难判断,所以在咱们的开发中,原则上应该避免使用全局变量。
要详细了解垃圾收集机制,建议阅读《JavaScript高级编程》中的4.3节