前言:栈(stack)、堆(heap)、队列(queue)三者皆属于JS中的数据结构,用于数据的存取操作
栈(stack):
-
特点:“LIFO,即后进先出(Last in,First out)”,执行数据存储时只能从顶部逐个存入,取出是也需要从顶部逐个取出。
-
例子模型【乒乓球盒子】:入口既出口,存和取只能通过一个入口操作,最先进入的存入到底部,最后存入的在在上面,取出是只能从上至下依次取出。
-
应用模型:通常情况下JS中使用Array进行栈的模拟,存储方法为pop()、和push()
var arr = [1, 2, 3, 4, 5]; arr.push(6); arr.pop();
堆(heap):
-
特点:“无序”的key—value “键值对”存储方式,【堆的存取方式跟顺序没有关系,不限出入口】
-
例子模型【书架存书】:要想在书架上找到想要的书,最直接的方式就是通过查找书名,书名是我们所必须的key,有了这把key,就可以轻松的检索到对应的书籍。
-
应用模型:使用Object对象进行堆的模拟存取?
队列(queue):
-
特点:“FIFO,即先进先出(First in,First out),数据存取时,”从队尾插入,从队头取出“,遵循的是一个出口一个入口的存取规则
-
例子模型【排队取餐】:后来的人依次排在队尾,先到的人先取餐
-
应用模型:使用Array数组模拟队列,存取方法push()【队尾推入】、shift()【队首取出】
var arr = [1, 2, 3, 4, 5]; arr.push(6); arr.shift();
栈、堆、队列在JavaScript中的应用
1.代码运行方式(栈应用/函数调用栈)
JavaScript中函数的调用过程可参考文章《》,其中执行栈(函数调用栈)就是用到栈的数据结构
JavaScript中函数的执行过程,其实就是一个入栈出栈的过程:
- 当脚本要调用一个函数时,JS解析器把该函数推入栈中(push)并执行
- 当函数运行结束后,JS解析器将它从堆中推出(pop)
2.内存存储(栈、堆)
JavaScript中变量类型有两种:
- 基础类型:(Number、String、Boolean、Null、Undefined、Symbol)一共6种
- 引用类型:Object(Map、Function、Array、RegExp)
JS中的基础数据类型,这些值都有固定的大小,往往都保存在栈内存中(闭包除外),由系统自动分配存储空间。我们可以直接操作保存在栈内存空间的值,因此基础数据类型都是按值访问 数据在栈内存中的存储与使用方式类似于数据结构中的堆栈数据结构,遵循后进先出的原则。
JS的引用数据类型,比如数组Array,它们值的大小是不固定的。引用数据类型的值是保存在堆内存中的对象。JS不允许直接访问堆内存中的位置,因此我们不能直接操作对象的堆内存空间。在操作对象时,实际上是在操作对象的引用而不是实际的对象。因此,引用类型的值都是按引用访问的。这里的引用,我们可以粗浅地理解为保存在栈内存中的一个地址,该地址与堆内存的实际值相关联。
3.事件轮询(队列)
JavaScript中事件轮询(Event Loop)的执行机制,就是采用队列的存取方式……
栈和堆引发的相关问题
1.传值与传址的区别、浅拷贝和深拷贝的由来
对于普通类型数据来书,将一个变量的值赋值给另一个变量,相当于在栈内存中创建了一个新的内存空间,然后从栈中复制值,存储到这个新空间中。对于基本类型,栈中存储的就是它自身的值,所以新内存空间存储的也是一个值。直接改变新变量的值,不会影响到旧变量的值,因为他们值存储的内存空间不同。
而对于引用类型来说,同样是复制栈中存储的值。但是栈存储的只是其引用地址,其具体的值存储在堆中。变量复制仅复制栈中存储的值,不会复制堆中存储的值,所以新变量在栈中的值是一个地址指针。
可见,变量复制与赋值,都属于栈存储拷贝,因此深浅拷贝可以这样区分:
- "浅拷贝:栈存储拷贝"
- "深拷贝:栈堆存储拷贝"
深拷贝会同时开辟新的栈内存,堆内存空间。
//深拷贝的代码实现逻辑解析!
2.函数传参是按值传递?还是引用传递
函数调用时,会对参数赋值。而参数传递过程其实同样是变量复制的过程,所以它是按值传递。
var person = person,因为传递参数是对象时,变量复制仅复制的栈存储(浅拷贝),所以修改对象属性会造成外部变量对象的修改。至此,当我们理清栈、堆数据结构,以及JS中数据类型存取方式。深浅拷贝问题也就通顺了。
3.内存空间
-
内存空间管理:
JavaScript执行过程中内存分配:
- 为变量对象分配需要的内存
- 在分配到的内存中进行读/写操作
- 不再使用时将其销毁,释放内存
内存管理不善,会出现内存泄露,造成浏览器内存占用过多,页面卡顿等问题。
-
垃圾回收机制:
JavaScript中有自动垃圾回收机制,会通过标记清除的算法识别哪些变量对象不再使用,对其进行销毁。开发者也可在代码中手动设置变量值为null(
a = null)进行标记清除,让其失去引用,以便下一次垃圾回收时进行有效回收。局部环境中,函数执行完成后,函数局部环境声明的变量不再需要时,就会被垃圾回收销毁(理想的情况下,闭包会阻止这一过程)。
全局环境只有页面退出时才会出栈,解除变量引用。所以开发者应尽量避免在全局环境中创建全局变量,如需使用,也要在不需要时手动标记清除,将其内存释放掉。
-
为什么会有栈内存和堆内存之分?
通常与垃圾回收机制有关。为了使程序运行时占用的内存最小。
当一个方法执行时,每个方法都会建立自己的内存栈,在这个方法内定义的变量将会逐个放入这块栈内存里,随着方法的执行结束,这个方法的内存栈也将自然销毁了。因此,所有在方法中定义的变量都是放在栈内存中的;
当我们在程序中创建一个对象时,这个对象将被保存到运行时数据区中,以便反复利用(因为对象的创建成本通常较大),这个运行时数据区就是堆内存。堆内存中的对象不会随方法的结束而销毁,即使方法结束后,这个对象还可能被另一个引用变量所引用(方法的参数传递时很常见),则这个对象依然不会被销毁,只有当一个对象没有任何引用变量引用它时,系统的垃圾回收机制才会在核实的时候回收它。
\