数据结构和 JavaScript 内存空间
堆-Heap
堆数据结构是一种树状结构,利用完全二叉树维护的一组无序数据,比如对象的key-value
数据。
堆分为最大堆和最小堆。
最大堆:将根节点最大的堆叫做最大堆或大根堆。最小堆:根节点最小的堆叫做最小堆或小根堆。
最大堆:
栈-Stack
栈是只能在某一端插入和删除的特殊线性表。
按照先进后出或后进先出(LIFO—last in first out)的原则存储数据,最先进入栈内的被压入栈底,最后的数据子栈顶。
入栈:往栈内添加元素
出栈:从栈内删除元素
使用数组实现一个栈
1 | /** |
栈的应用
判断括号是否合法
思路:如果是(
就入栈,如果是)
就查看栈是否为空,如果栈为空就不合法,如果不为空,就弹出一个栈顶元素。循环结束,查看栈是否为空,为空就是合法,否则就不合法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19function is_leagl_brackets(string) {
let stack = new MyStack();
for (let i = 0; i < string.length; i++) {
let str = string[i];
if (str === "(") {
stack.push(str);
} else if (str === ")") {
if (stack.isEmpty()) {
return false;
} else {
stack.pop();
}
}
}
return stack.isEmpty();
}
console.log(is_leagl_brackets("()()))"));//false
console.log(is_leagl_brackets("(sd(qwqw)sd(sd))"));//true
console.log(is_leagl_brackets("()()sd()(sd()fw))("));//false计算逆波拉表达式
逆波兰表达式也叫后缀表达式,将复杂的表达式转换为可以依靠简单的操作得到计算结果的表达式,例如(a+b)*(c+d)
转换为ab+cd+*
根据简单的逆波兰表达式数组,计算表达式结果。
思路:如果是数字就入栈,如果是符号(只是简单的例子,不考虑错误的情况)就连续弹出两个栈顶元素,用两个元素和操作符组成计算表达式(第二个元素在操作符左边),利用eval
计算结果,取整,变成字符串再放入栈中。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function calc_exp(arr) {
let stack = new MyStack();
arr.forEach((item) => {
if (["+", "-", "/", "*"].indexOf(item) > -1) {
let v1 = stack.pop();
let v2 = stack.pop();
let exp = v2 + item + v1;
let result = parseInt(eval(exp));
stack.push(result.toString());
} else {
stack.push(item);
}
});
return stack.pop();
}
console.log(calc_exp(["4", "13", "5", "/", "+"]));//6实现一个有 min 方法的栈
思路:利用两个栈,一个存放数据,一个存放最小值1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35function MinStack() {
let data_stack = new MyStack(); //存储数据
let min_stack = new MyStack(); //存储最小值
//入栈
this.push = function (item) {
data_stack.push(item);
//如果最小值栈是空的或当前元素小于最小栈顶元素,就入栈到最小栈顶
if (min_stack.isEmpty() || item < min_stack.top()) {
min_stack.push(item);
} else {
//如果当前值大于最小值栈顶元素,就把最小值栈顶元素再次入栈
min_stack.push(min_stack.top());
}
};
//出栈
this.pop = function () {
min_stack.pop();
return data_stack.pop();
};
//最小值
this.min = function () {
return min_stack.top();
};
}
let minStack = new MinStack();
minStack.push(3);
console.log(minStack.min()); //3
minStack.push(6);
console.log(minStack.min()); //3
minStack.push(9);
console.log(minStack.min()); //3
minStack.push(1);
console.log(minStack.min()); //1
minStack.pop();
console.log(minStack.min()); //3
队列-Queue
队列也是一种操作受限制的线性表。
只允许在前端(头)进行删除操作,在后端(尾)进行插入操作。所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出(FIFO—first in first out)
队列没有元素时,成为空队列。
入队:往队列中插入元素
出队:从队列中删除元素
使用数组实现一个队列
1 | /** |
队列的应用
约瑟夫环
题目:一个数组a[100]
存放0-99
,要求每隔两个数删掉一个数,到末尾时循环至开头继续执行(删除逻辑也是继续),求最后一个被删掉的数。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function circle(arr_list) {
let queue = new MyQueue();
arr_list.forEach((v) => queue.enqueue(v));
let index = 0;
while (queue.size() > 1) {
let item = queue.dequeue();
index++;
if (index % 3 != 0) {
queue.enqueue(item);
}
}
return queue.head();
}
let arr_list = new Array(100).fill(0).map((v, i) => i);
console.log(circle(arr_list));斐波那契数列
公式:fn= f(n-2)+f(n-1)
前两位是 1,11
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23function fib(n) {
if (n === 1 || n === 2) return 1;
let queue = new MyQueue();
//初始值前两位是1
queue.enqueue(1);
queue.enqueue(1);
let index = 2;
while (index < n) {
//弹出队头
let num1 = queue.dequeue();
//获取队头
let num2 = queue.head();
//计算下一个数据
let num3 = num1 + num2;
//将新数据入队
queue.enqueue(num3);
index++;
}
//弹出队头
queue.dequeue();
return queue.head();
}
console.log("fib", fib(5));
链表
JavaScript 内存空间
内存空间管理
JavaScript 的内存生命周期:
- 为变量分配所需要的内存
- 使用分配到的内存(读、写)
- 不再使用时将其销毁,释放内存
内存管理不善,会出现内存泄漏,造成浏览器内存占用过多,页面卡顿等问题。
例子:
1 | var a = 20; // 在内存中给数值变量分配空间 |
垃圾回收机制
JavaScript 有自动的垃圾回收机制,会通过标记清除的算法,找出不再继续使用的变量对象,释放其内存。
开发者也可通过设置a=null
手动标记清除,让其原本对应的值失去引用,脱离执行环境,这个值会在下一次垃圾回收执行时找到并释放。而在适当的时候解除引用,是为页面获得更好性能的一个重要方式。
局部环境中,函数执行完成后,函数局部环境声明的变量不再需要时,就会被垃圾回收销毁(理想的情况下,闭包会阻止这一过程)。
全局环境只有页面退出时才会出栈,解除变量引用。所以开发者应尽量避免在全局环境中创建全局变量,如需使用,也要在不需要时手动标记清除,将其内存释放掉。
简单介绍一下 V8 引擎的垃圾回收机制
v8 的垃圾回收机制基于分代回收机制,这个机制又基于世代假说,这个假说有两个特点,一是新生的对象容易早死,另一个是不死的对象会活得更久。基于这个假说,v8 引擎将内存分为了新生代和老生代。
新创建的对象或者只经历过一次的垃圾回收的对象被称为新生代。经历过多次垃圾回收的对象被称为老生代。
新生代被分为 From 和 To 两个空间,To 一般是闲置的。当 From 空间满了的时候会执行 Scavenge 算法进行垃圾回收。当我们执行垃圾回收算法的时候应用逻辑将会停止,等垃圾回收结束后再继续执行。这个算法分为三步:
(1)首先检查 From 空间的存活对象,如果对象存活则判断对象是否满足晋升到老生代的条件,如果满足条件则晋升到老生代。如果不满足条件则移动 To 空间。
(2)如果对象不存活,则释放对象的空间。
(3)最后将 From 空间和 To 空间角色进行交换。
新生代对象晋升到老生代有两个条件:
(1)第一个是判断是对象否已经经过一次 Scavenge 回收。若经历过,则将对象从 From 空间复制到老生代中;若没有经历,则复制到 To 空间。
(2)第二个是 To 空间的内存使用占比是否超过限制。当对象从 From 空间复制到 To 空间时,若 To 空间使用超过 25%,则对象直接晋升到老生代中。设置 25% 的原因主要是因为算法结束后,两个空间结束后会交换位置,如果 To 空间的内存太小,会影响后续的内存分配。
老生代采用了标记清除法和标记压缩法。标记清除法首先会对内存中存活的对象进行标记,标记结束后清除掉那些没有标记的对象。由于标记清除后会造成很多的内存碎片,不便于后面的内存分配。所以了解决内存碎片的问题引入了标记压缩法。
由于在进行垃圾回收的时候会暂停应用的逻辑,对于新生代方法由于内存小,每次停顿的时间不会太长,但对于老生代来说每次垃圾回收的时间长,停顿会造成很大的影响。 为了解决这个问题 V8 引入了增量标记的方法,将一次停顿进行的过程分为了多步,每次执行完一小步就让运行逻辑执行一会,就这样交替运行。
哪些操作会造成内存泄漏
意外的全局变量
未定义的变量会在全局对象创建一个新变量。在浏览器中,全局对象是 window 。1
2
3function foo(){
bar='bar is not defined'
}函数内部通过 this 创建全局变量。
1
2
3function foo(){
this.bar='bar is not defined'
}可以通过开启严格模式避免这种错误。
全局变量注意事项
如果必须使用全局变量存储大量的数据时,确保用完以后把它设置成 null。被遗忘的计时器或回调函数
1
2
3
4
5
6
7let someResource=getData();
setInterval(()=>{
let node=document.getElementById('node');
if(node){
node.innerHTML=JSON.stringify(someResource)
}
},1000)如果 id 为 Node 的元素从 DOM 中移除, 该定时器仍会存在, 同时, 因为回调函数中包含对 someResource 的引用, 定时器外面的 someResource 也不会被释放.
脱离 DOM 的引用
进行 DOM 操作的时候,用变量保存 DOM 结构,这样一个 DOM 结构就存在两个引用,一个是 DOM 树,一个是变量。如果删除 DOM 树中的元素,因为还存在变量引用,内存不会释放。所以删除时,需要设置变量为 null1
2
3
4
5
6
7
8
9let button=document.getElementById('button');
function doStuff(){
button.click()
}
function removeButton(){
document.body.removeChild(document.getElementById('button'))
}还要考虑 DOM 树内部或子节点的引用问题。假如你的 JavaScript 代码中保存了表格某一个
的引用。将来决定删除整个表格的时候,直觉认为 垃圾回收器(GC)会回收除了已保存的 以外的其它节点。实际情况并非如此:此 是表格的子节点,子元素与父元素是引用关系。由于代码保留了 的引用,导致整个表格仍待在内存中。保存 DOM 元素引用的时候,要小心谨慎。 不合理的使用闭包,从而导致某些变量一直被留在内存当中
变量赋值 null