js中的栈、堆、队列、内存空间

栈(stack) 、堆(heap)、 队列(queue)是js的三种数据结构。前端

栈(stack)

栈的特色是"LIFO,即后进先出(Last in, first out)"。数据存储时只能从顶部逐个存入,取出时也需从顶部逐个取出。《前端进击的巨人(一):执行上下文与执行栈,变量对象》中解释执行栈时,举了一个乒乓球盒子的例子,来演示栈的存取方式,这里再举个栗子搭积木。算法

举个栗子:乒乓球盒子/搭建积木
segmentfault

JavaScript中Array数组模拟栈:数组

var arr = [1, 2, 3, 4, 5];

arr.push(6); // 存入数据 arr -> [1, 2, 3, 4, 5, 6]
arr.pop();   // 取出数据 arr -> [1, 2, 3, 4, 5]

 

堆(heap)

堆的特色是"无序"key-value"键值对"存储方式。浏览器

举个栗子:书架存书数据结构

咱们想要在书架上找到想要的书,最直接的方式就是经过查找书名,书名就是咱们的key。拿着这把key,就能够轻松检索到对应的书籍。闭包

"堆的存取方式跟顺序没有关系,不局限出入口"函数

 

队列 (queue)

队列的特色是是"FIFO,即先进先出(First in, first out)" 。
数据存取时"从队尾插入,从队头取出"oop

"与栈的区别:栈的存入取出都在顶部一个出入口,而队列分两个,一个出口,一个入口"spa

举个栗子:排队取餐

JavaScript中Array数组模拟队列:

var arr = [1, 2, 3, 4, 5];

// 队尾in
arr.push(6);    // 存入 arr -> [1, 2, 3, 4, 5, 6]
// 队头out
arr.shift();    // 取出 arr -> [2, 3, 4, 5, 6]

 

栈、堆、队列在JavaScript中的应用

1. 代码运行方式(栈应用/函数调用栈)

《前端进击的巨人(一):执行上下文与执行栈,变量对象》详解了JavaScript运行时的函数调用过程,而其中执行栈(函数调用栈)就是用到栈的数据结构。

JavaScript中函数的执行过程,其实就是一个入栈出栈的过程:

  1. 当脚本要调用一个函数时,JS解析器把该函数推入栈中(push)并执行
  2. 当函数运行结束后,JS解析器将它从堆栈中推出(pop)

具体执行过程可翻阅上篇文章《前端进击的巨人(一):执行上下文与执行栈,变量对象》,这里再也不赘述。

2. 内存存储(栈、堆)

JavaScript中变量类型有两种:

  1. 基础类型(Undefined, Null, Boolean, Number, String, Symbol)一共6种
  2. 引用类型(Object)

JS中的基础数据类型,这些值都有固定的大小,每每都保存在栈内存中(闭包除外),由系统自动分配存储空间。咱们能够直接操做保存在栈内存空间的值,所以基础数据类型都是按值访问 数据在栈内存中的存储与使用方式相似于数据结构中的堆栈数据结构,遵循后进先出的原则。

JS的引用数据类型,好比数组Array,它们值的大小是不固定的。引用数据类型的值是保存在堆内存中的对象。JS不容许直接访问堆内存中的位置,所以咱们不能直接操做对象的堆内存空间。在操做对象时,其实是在操做对象的引用而不是实际的对象。所以,引用类型的值都是按引用访问的。这里的引用,咱们能够粗浅地理解为保存在栈内存中的一个地址,该地址与堆内存的实际值相关联。

 

3. 事件轮询(队列)

JavaScript中事件轮询(Event Loop)的执行机制,就是采用队列的存取方式,因事件轮询(Event Loop)也是JS基础中的一个比较难理解的知识点,后续另开一篇章再做详细探究。

 

深浅拷贝

将一个变量的值赋值给另外一个变量,至关于在栈内存中建立了一个新的内存空间,而后从栈中复制值,存储到这个新空间中。对于基本类型,栈中存储的就是它自身的值,因此新内存空间存储的也是一个值。直接改变新变量的值,不会影响到旧变量的值,由于他们值存储的内存空间不一样。

// 基本类型复制变量
var a = 10;
var b = a;
b = 20;

a // 10
b // 20

而对于引用类型来讲,一样是复制栈中存储的值。可是栈存储的只是其引用地址,其具体的值存储在堆中。变量复制仅复制栈中存储的值,不会复制堆中存储的值,因此新变量在栈中的值是一个地址指针。

// 引用类型复制变量
var a = { age: 27 };
var b = a;
b.age = 29;

a.age == b.age; // 29

可见,变量复制赋值,都属于栈存储拷贝,所以深浅拷贝能够这样区分:

  • "浅拷贝:栈存储拷贝"
  • "深拷贝:栈堆存储拷贝"

深拷贝会同时开辟新的栈内存,堆内存空间。

// 利用JSON对象方法实现深拷贝
var a = { age: 27 };
var b = JSON.parse(JSON.stringify(a));
b.age = 29;

a.age // 27
b.age // 29

函数传参数是按值传递?按引用传递?

var person = {
 age: 27
};
function foo (person) {
  person.age = 29;
}
foo(person);
person.age // 29;

函数调用时,会对参数赋值。而参数传递过程其实一样是变量复制的过程,因此它是按值传递。var person = person,由于传递参数是对象时,变量复制仅复制的栈存储(浅拷贝),因此修改对象属性会形成外部变量对象的修改。

至此,当咱们理清栈、堆数据结构,以及JS中数据类型存取方式。深浅拷贝问题也就通顺了。

 

内存空间

内存空间管理

JavaScript执行过程当中内存分配:

  1. 为变量对象分配须要的内存
  2. 在分配到的内存中进行读/写操做
  3. 再也不使用时将其销毁,释放内存

内存管理不善,会出现内存泄露,形成浏览器内存占用过多,页面卡顿等问题。

 

垃圾回收机制

JavaScript中有自动垃圾回收机制,会经过标记清除的算法识别哪些变量对象再也不使用,对其进行销毁。开发者也可在代码中手动设置变量值为null(a = null)进行标记清除,让其失去引用,以便下一次垃圾回收时进行有效回收。

局部环境中,函数执行完成后,函数局部环境声明的变量再也不须要时,就会被垃圾回收销毁(理想的状况下,闭包会阻止这一过程)。

全局环境只有页面退出时才会出栈,解除变量引用。因此开发者应尽可能避免在全局环境中建立全局变量,如需使用,也要在不须要时手动标记清除,将其内存释放掉。

 

为何会有栈内存和堆内存之分?

一般与垃圾回收机制有关。为了使程序运行时占用的内存最小。

当一个方法执行时,每一个方法都会创建本身的内存栈,在这个方法内定义的变量将会逐个放入这块栈内存里,随着方法的执行结束,这个方法的内存栈也将天然销毁了。所以,全部在方法中定义的变量都是放在栈内存中的;

当咱们在程序中建立一个对象时,这个对象将被保存到运行时数据区中,以便反复利用(由于对象的建立成本一般较大),这个运行时数据区就是堆内存。堆内存中的对象不会随方法的结束而销毁,即便方法结束后,这个对象还可能被另外一个引用变量所引用(方法的参数传递时很常见),则这个对象依然不会被销毁,只有当一个对象没有任何引用变量引用它时,系统的垃圾回收机制才会在核实的时候回收它。

相关文章
相关标签/搜索