S01E01-从堆、栈、内存机制开始

JavaScript 中有三种数据结构: 栈(stack) 、堆(heap)、 队列(queue)。它们是我们理解 JavaScript 核心的基础。

本篇将围绕栈(stack) 、堆(heap),以及 JavaScript 的内存机制来展开。队列(queue)将会放在本系列的第四部分-异步和性能来讲解。

栈(stack)

栈(stack)有三层含义:

含义一:数据结构

栈的第一种含义是表示的是数据的存放方式。

栈存储数据的特点:LIFO规则,即后进先出(Last In, First Out)。数据存储时只能从顶部逐个存入,取出时也只能从顶部逐个取出。顶部是唯一的出口。

借助前端大神的乒乓球盒子的栗子:

image

如图所示,我们只能从栈顶取出或放入乒乓球,最先放进盒子的总是最后才能取出。
乒乓球的放入/取出,在栈中可称为入栈/出栈

含义二:函数调用栈(call stack)

stack 的第二层含义是代码的一种运行方式。
通过栈的方式来管理代码的执行顺序,是栈数据结构的一种实践,遵循LIFO规则。

含义三:内存空间

stack 的第三种含义是存放数据的一种内存区域。
在 JS 运行时,需要内存空间存放数据。一般来说,内存空间又被分为两种:栈内存(stack)、堆内存(heap)。

栈内存的特点:

  • 一般存放基本类型的值引用类型的引用地址(指针)
  • 是有序的
  • 在内存中占据空间小,大小固定

例如,最简单的,声明一个变量a:

1
var a = 12 

如图所示,会在栈内存中开辟一块空间存储 12,把存储的 12 赋值给 a。

1

我们需要注意的是:

JS 允许直接操作保存在栈内存中的值。因此,基本类型是按值访问的

堆(heap)

堆只有一层含义:内存空间。堆内存的特点:

  • 一般存放引用类型的值
  • 是无序的
  • 引用类型的值没有固定大小,可扩展(一个对象我们可以添加多个属性),占据空间大

为了更好的理解堆内存空间,我们看一个最简单的:

1
var obj = { m : 20 }

声明一个变量 obj,会在堆内存中开辟一块新的空间,把对象中的键值对依次存储进来(同时,为这个空间加了一个16 进制的地址的标记),这个地址和这块空间是关联在一起的,如图所示。
注意:这个空间地址是被保存在栈内存中的。

2

我们需要注意的是:

JS 不允许直接访问堆内存中的位置。在操作对象时,实际上是操作的是对象的引用。因此,引用类型是按引用访问的

内存空间管理

不管是栈内存,还是堆内存,都是由系统自动分配和自动释放的。了解内存的管理机制,对于提高我们的页面性能尤其重要。

内存的生命周期一般有三步:

  • 分配:当我们声明变量、函数、对象时,系统会自动为它们分配内存
  • 使用:即读/写内存,也就是使用变量、函数等
  • 回收:使用完毕,由垃圾回收机制自动回收不再使用的内存

分配和使用都很好理解。对于内存的释放回收,我们接下来重点看一下。

垃圾回收机制

垃圾回收机制:
浏览器会在空闲时,遍历所有的内存空间,发现谁不被占用,就会自主的进行内存回收。

该机制的核心思想就是找到谁不被使用,因此我们可以通过标记清除的算法来标记哪些内存不再被占用。

  1. 对于堆内存,我们可以将占用它的变量手动赋值为 null 来标记清除。
  2. 对于栈内存,局部环境中,只有当函数执行完成后,函数局部环境声明的变量不再需要时,才会被释放(特殊不销毁的情况:闭包)。全局环境只有当页面关闭时才会解除变量引用。因此,开发者应尽量避免创建全局变量。

垃圾回收算法除了”标记清除”,还有一种”引用计数”,不常用,仅作了解。

内存泄漏

由于疏忽或错误造成程序未能释放那些已经不再使用的内存,造成内存的浪费。
引起内存泄漏的情况:

  • 在函数内部,不带var声明变量,给 window 添加了属性
    1
    2
    3
    4
    5
    function foo() {
    this.a = 'window.a';
    b = '全局变量';
    }
    foo();
  • 当不需要 setInterval 或者 setTimeout 时,定时器没有被 clear,定时器的回调函数以及内部依赖的变量都不能被回收,造成内存泄漏。
  • 闭包可以保存内部状态,使其得不到释放,造成内存泄漏。
  • 没有清理 DOM 元素引用,手动清除为 null 即可

结束

重学 JS 系列 预计 25 篇左右,这是一个旨在帮助大家,其实也是帮助我自己捋顺 JavaScript 底层知识的系列。主要包括变量和类型、执行上下文、作用域及闭包、原型和继承、单线程和异步、JS Web API、渲染和优化几个部分,将重点讲解如执行上下文、作用域、闭包、this、call、apply、bind、原型、继承、Event-loop、宏任务和微任务等比较难懂的部分。让我们一起拥抱整个 JavaScript 吧。

大家或有疑问、或指正、或鼓励、或感谢,尽管留言回复哈!非常欢迎 star 哦!

作者

ZhangLK

发布于

2021-01-01

更新于

2023-07-05

许可协议