内存与数据结构

把一个页面看做一个完整的独立应用。

那么,哪些个体参与了应用,个体以什么形式存在的,个体存放在哪里,个体在内存中如何存放?

个体 -> 数据类型 -> 内存 -> 数据结构

个体

个体可以理解为角色,这些角色会参与应用运行
有变量、函数、对象

1
2
3
4
5
6
// 申明变量
var a = 10;
// 申明函数
function add() {}
// 申明对象
class M {}

a、add、M 表示个体的名字。我们可以通过名字,访问到具体的值。

数据类型

7 种基础数据类型和一种引用类型

基础数据类型

基础数据类型:Boolean、Null、Undefined、Number、String、BigInt、Symbol

重点理解 基础数据类型的值,是不可变的

1
2
3
let a = 1;
let b = a;
b++;

将 a 赋予 b,这时候 a 和 b 是等价的。随后 b++,b 的值被改变了,a 的值却没有改变。这意味着 a b 的等价,并不表示他们是同一个值。也就是说,a 赋予 b 的时候,重新给 b 分配了一块内存空间。因此我们说,基础数据类型,是按值访问的。用图表示
pic.1708326860642
基础数据类型的比较,是值在比较

1
2
3
const a = 1;
const b = 1;
a === b; // true

当 a 和 b 在比较的时候,本质上是值在比较。

还有一点要注意,当我们使用字符串时,竟然可以使用方法,这是因为访问字符串时,实际上依然是在访问一个对象。使用字符串调用方法时,经历了以下 3 步骤

1
2
3
4
5
6
7
8
// 首先使用包装对象创建对象
var _str = new String('hello world');

// 然后使用包装对象的实例去访问方法
_str.charAt(0);

// 最后销毁该对象
_str = null;

引用数据类型

与基础数据类型不同,引用类型的值是可以被改变的

1
2
3
4
5
6
7
8
9
const book = {
title: 'JavaScript 核心进阶',
author: '这波能反杀',
date: '2020.08.02',
};
const b2 = book;
b2.author = '反杀';
console.log(book); // {title: "JavaScript 核心进阶", author: "反杀", date: "2020.08.02"}
console.log(b2); // {title: "JavaScript 核心进阶", author: "反杀", date: "2020.08.02"}

我们发现,修改变量 b2 的值,变量 book 也被修改了。这是因为执行 b2=book 时,b2 拷贝的是 book 的地址,所以他们指向的是同样的引用类型值。我们可称之为浅拷贝。
pic.1708326879449

所以说,引用数据类型,是按引用访问的。这里的引用,指的就是内存中的地址。

地址:我们常说的地址,其实指的是内存中的首地址,因为一个引用对象占多少个内存格子是不确定的。内存管理器知道这个引用的大小,那么就能找到首尾地址。

引用类型的比较,本质上是他们的内存地址在比较

1
2
3
4
5
6
7
8
9
10
11
const a = {
name: '张三',
age: 20,
};
const b = a;
b.name = '李四';
a === b; // true

const c = {};
const d = {};
c === d; // false

由于 c 和 d 是两个分开创建的对象,所以他们在内存中的地址是不一样的,所以结果也不同。直接比较引用地址,我们可以称之为浅比较。

还有一种场景,只比较一层数据结构,也被认为是浅比较,这种比较成本比较低,浅拷贝同理。

数据类型判断

判断是否为数组

  1. Array.isArray(arr)
  2. arr instanceof Array
  3. Object.prototype.toString.call(arr) === ‘[object, Array]’

判断是否为对象

  1. isObject
1
2
3
4
function isObject(value) {
const type = typeof value;
return value != null && (type === 'object' || type === 'function');
}
  1. Object.prototype.toString.call(obj) === ‘[object Object]’
  2. obj instanceof Object 数组也成立

内存

运行一段 app/进程,操作系统会分配一段连续的内存空间,在内存中运行。

内存用于储存程序运行时的信息。CPU 通过寄存器直接访问,访问速度非常快。
硬盘可以将大量电影、图片等信息永久存储在硬盘。CPU 不能直接访问硬盘的数据,要通过硬盘控制器来访问。

我们可以将一些数据做持久化储存,也就是常说的本地缓存,其实就是将数据存储在硬盘。在浏览器中,提供了 localStorage 对象来帮助我们实现本地缓存。

内存和硬盘的区别:

对象 容量 访问速度 CPU 能否直接访问 存储时效
内存 程序运行时
硬盘 不能 持久性

JS 中,内存分为栈内存和堆内存。栈内存与堆内存本身没有区别,只因为存取方式的差异,而有了不同。

使用栈内存时,从地址高位开始分配内存空间。使用堆内存时,从地址低位开始分配。

:::info
内存中的地址本应该用 16 进制表示,为了便于理解,此处用 10 进制表示。
:::
pic.1708326895926

内存空间

Bit-比特
Byte-字节 计算机数据类型的最基本单位。1 Byte = 8 Bits
KB-千字节
MB-兆字节
GB-吉字节
TB-太字节

1 GM = 1024 MB
1 TB = 1024 GB

英文字母 占一个字节空间
中文汉字 占两个字节空间
英文标点符号 占一个字节空间
中文标点符号 占两个字节

指针、引用、对象

指针:也叫做首地址。指针占 4 个字节
0xFF
[1][2][3][4][][][][]

引用:指针的别名。在我们前端里,可以认为指针和引用是同个东西。

举个例子:
基础数据类型:

1
2
var a = 10;
a = 20;

0xFF(十六进制。我们这里用十进制代替,更直观)
[10][20][][][][][][]

改为 20 后,引用变量 a 的指针发生改变。10 失去了引用,等待被回收。所以我们说基础数据类型的值是不可变的。
0xFF
[10][20][][][]

引用数据类型:

1
var b = 首地址 -> { m: 10 }

首地址也是个基础数据类型。当我们改变引用类型时,首地址不变。

基础数据结构

基础数据结构主要介绍一下栈、堆、队列

说到栈,其实有两种场景,栈数据结构和栈内存空间。

第一种场景,栈数据结构:表达的是对数据的一种存取方式。

把栈数据结构的存取方式,通过乒乓球盒子来分析
pic.1708326910241
乒乓球依次入栈,5 号球位于最顶层,他一定是最后放进去的。1 号球位于最底层,你要想拿 1 号球,必须将上面所有乒乓球取出来之后才能取出。但 1 号球是最先放入盒子的。

这种存取方式与栈数据结构如出一辙。特点可以总结为先进后出,后进先出(LIFO Last in, First Out)。

javaScript 中,数组提供了两个栈方法来应对这种存取方式。

push:向数组末尾添加元素「进栈方法」
pop:删除数组末尾元素「出栈方法」

第二种场景,栈内存空间。刚刚提到,内存空间,因为操作方式不同才有了区别,而栈内存空间的操作方式,正是使用了栈数据结构的思维。

栈内存空间,用于记录函数的执行记录,管理函数的执行顺序。我们称他为函数调用栈「call stack」

应用场景:有效的括号、两个栈实现队列

堆数据结构:是树结构中的一种。常见的是二叉堆。

二叉堆分为两种类型:最大堆和最小堆

最大堆:堆顶元素是整个堆中的最大值。任何父节点的键值都大于任何一个子节点
最下堆:堆顶元素是整个堆中的最小值。任何父节点的键值都小于任何一个子节点

以最小堆为例:

  • 插入节点时,只能插入二叉堆的最后一个位置。比较节点时,如果插入节点比父节点大,则需要上浮,持续比较,直到比父节点小为止
  • 删除节点时,我们通常只会删除堆顶的元素。删除后结构出现了混乱,需要将最后一个节点补充到堆顶。补充后树结构不符合最小堆的特性,因此需要与子元素进行比较,找到最小的子元素与其交换位置,这个行为称之为下沉。持续比较,知道符合最小堆的规则

应用场景:
数组的 sort 方法,也是采用类似堆的排序逻辑;前中后序遍历;

1
2
3
4
5
6
7
8
9
function sort(a, b) {
// 最小堆(升序)
return a < b;
}

function sort(a, b) {
// 最大堆(降序)
return a > b;
}

栈和堆的区别

  • 分配方式不同:栈有操作系统分配;堆由开发人员分配
  • 存放内容不同:栈用于存放函数的参数值、局部变量、函数返回值等;堆的存放内容由开发人员填充
  • 释放方式不同:栈内存的数据,随生命周期函数的调用结束而结束。堆内存由开发人员释放,容易产生内存泄漏。
  • 空间大小不同:每个进程拥有的栈大小远远小于堆大小。理论上,栈只有几兆大小,堆可以是虚拟内存的大小。

队列

先进先出的思维。在浏览器中,所有的异步事件都是放在任务队列中。

优先级队列是一个重要的概念,决定了哪个任务优先执行。

运用到实践中,有如下常用操作:

  1. 从队列最后入队
  2. 从队列头部出队
  3. 从队列任意位置离队(有其他事情)
  4. 从队列任意位置插队(特殊权利)
  5. 清空队列

应用场景:先到先处理

  • 医院挂号
  • promiseJobs

链表

链表是一种递归的数据结构,由多个节点组成,节点之前使用引用相互关联,组成一根链条。

链表的特征:

  • 在内存中,链表是松散不连续的结构,通过引用确定节点之间的联系,不像数组那样是排列在一起的连续内存地址
  • 链表没有序列,如果引用是单向的,只能通过上一个节点,找到下一个节点
  • 节点之间的引用可以是单向的「单向链表」,也可以是双向的「双向链表」,还可以是首尾连接的「循环链表」

和数组的区别:

  • 在内存空间里,链表是松散的,不连续的。数组是紧密的,连续的
  • 在性能角度考虑,访问某个成员,数组远远优于链表,而新增/删除元素,链表远远优于数组

应用场景:

  1. 节点元素的相互引用,nextSibling、previousSibling
  2. 原型链中,由__proto__进行关联的单向链表
  3. react 源码中的Fiber节点中的nextEffect

内存空间管理

内存溢出:多指栈溢出

内存泄露:指某段内存空间无法被管理

  1. 没有引用,无法被访问
  2. 失去引用时,没被标记,无法被垃圾回收机制回收

思考

基础数据类型存在栈内存,引用数据类型存在堆内存,这句话对不对?
不对。栈内存和堆内存本身并没有区别,只是存取方式的差异而有了不同。基础数据类型可以在栈里,也可以在堆里,函数执行时的参数、变量都在栈里。引用类型的地址在栈里,引用类型的数据在堆里。

变量会存在内存中吗,如果存在,以什么方式存在
不存在。内存中只存在 16 进制的地址编码和具体的数值

m 的值是否被改变?为什么?

1
2
3
4
5
6
7
8
const m = {
a: 1,
b: 2,
};
function foo(arg) {
arg.a = 20;
}
foo(m);

m 被改变了。函数传入一个引用数据类型时,会干扰外部的值,参数变量的地址入栈内存,引用类型对象入堆内存。

为什么使用不可变数据类型:
因为浅比较无法比较出引用数据类型之间的差异

引用类型为什么有地址这个概念?
因为一个内存空间无法记录整个对象的值,所以用首地址(地址)来记录

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×