内存空间
常用数据结构:
- 栈数据结构:后进先出(LIFO)
- 堆数据结构
- 队列:先进先出(FIFO),事件循环的基础结构
JS内存空间:
- 栈(stack):存放变量
- 堆(heap):存放复杂对象
- 池:一般归为栈,存放常量
注意:闭包中的变量不存放在栈中,而是存放在堆中!!
变量的存放:
- 基本数据类型:保存在栈中
- 引用数据类型:对象保存在堆内存中,因为JS不允许直接访问堆内存中的位置,因此在操作对象时实际是在操作对象的引用(即保存在栈中的一个地址)而不是实际的对象
QUESTION:为什么会有栈内存和堆内存的区别?
由于垃圾回收机制,为了使程序运行所占用的空间最小。
var a = {n: 1}; var b = a; a.x = a = {n: 2}; a.x // 这时 a.x 的值是多少 b.x // 这时 b.x 的值是多少
上面这个问题的结果:a.x值为undefined,b.x的值为{n:2}
重点在 a.x = a = {n:2};这句!
赋值运算是从左到右解析:
a.x = a [ = undefined ] 得到两个引用,a.x表示在{n:1}这个对象中新增加了一个x
a = {n:2}
从右到左赋值:
a = {n:2} 这句的意思是给a重新赋值,将a的引用指向新的对象,即指向{n:2}
a.x = ( a = {n:2} ) 此时将{n:2}赋值给x,此时,由于b指向的地址没有变,则b的当前对象为{ n:1, x: {n:2} }
so
a.x值为undefined,因为新的a里没有对象x
b.x值为{n:2}
内存回收
局部变量和全局变量的销毁:
- 在函数执行完后,局部变量就没存在的必要了,垃圾回收器可以很容易的做出判断并回收
- 全局变量什么时候自动释放内存空间很难判断,所以要尽量避免使用全局变量。如果必须使用全局变量存储大数据时,确保使用完之后将它设置为null或重新定义
垃圾回收算法的核心:如何判断内存已经不再使用了。
垃圾回收算法:引用计数(目前只有老IE在用,);标记清除【Mark-and-sweep】(现代浏览器,常用)
引用计数算法定义“内存不再使用”的标准是:看一个对象是否有指向它的引用,如果没有其他对象指向它,即引用数为0,就说明该对象已经不再需要了,可以释放该内存。
如果一个值不需要了,但是引用数却不为0,那么垃圾回收机制就没法释放这块内存,从而导致内存泄露。
let arr = [1, 2, 3, 4];
console.log('hello world');
上面这个代码中,[1,2,3,4]是一个值,会占用内存,变量arr是对这个值的引用,so引用数为1,尽管后面的代码没有用到arr,它还是会持续占用内存。
如果在最后一行增加一句:arr = null; 则解除了arr对[1,2,3,4]的引用,这块内存就可以被释放了。
// 创建一个对象person,他有两个指向属性age和name的引用 var person = { age: 12, name: 'aaaa' }; person.name = null; // 虽然name设置为null,但因为person对象还有指向name的引用,因此name不会回收 var p = person; person = 1; //原来的person对象被赋值为1,但因为有新引用p指向原person对象,因此它不会被回收 p = null; //原person对象已经没有引用,很快会被回收
上面的例子很好的解释了什么是“没有其他对象指向它”
引用计数还有一个问题是:循环引用!如果两个对象相互引用,尽管它们不再使用了,但是还是不会回收,从而造成内存泄露。
标记清除算法将“不再使用的对象”定义为“无法到达的对象”。从根部(全局对象)开始,凡是能从根部到达的对象,都是还需要使用的!无法由根部出发触及到的对象(什么意思?!)(包含没有任何引用的对象)被标记为不再使用,稍后进行回收。
现代的垃圾回收器改良算法,本质为:可达内存被标记,其余的被当做垃圾回收
var div = document.createElement("div"); div.onclick = function() { console.log("click"); };
这个例子我没搞懂为啥在标记清除算法下就可以回收,在引用计数算法下不能回收?!
QUESTION:从内存来看null和undefined本质区别是什么??
null就是一切虚无,什么都没有
undefined是里面有东西,但是未知
WeakMap和WeakSet
ES6提出的新的数据结构,它们对于值的引用都是不计入垃圾回收机制的。(???啥意思???)
const wm = new WeakMap(); const element = document.getElementById('example'); wm.set(element, 'some information'); wm.get(element) // "some information"
以上代码新建一个WeakMap实例,将一个DOM节点作为键名存入该实例,‘some information’作为键值一起存放在WeakMap里。
这时,WeakMap对element的引用就是弱引用,即DOM节点对象的引用计数是1,这时,一旦消除对该节点的引用,它占用的内存就会被释放,WeakMap保存的这个键值对也会自动消失。
常见的JavaScript内存泄露:
- 意外的全局变量:比如在函数内部变量忘记用var声明,实际上JS会把这个变量挂载到全局对象上,这样就意外创建了一个全局变量!
- 解决办法:使用'use strict'严格模式
- 被遗忘的计时器或回调函数
- 注意点:定时器要及时remove
- addEventListener这类观察者的例子,目前现代浏览器能很好的检测和处理循环引用,因此在处理回收节点内存的问题时,不需要非要调用removeEventListener
- 脱离DOM的引用
- 如果把多个DOM存到一个对象里,即使删除了这些DOM节点,但是对象里还是有对DOM的引用,因此无法回收
- 解决:在删除DOM前,把关于这个DOM的引用(比如绑定事件)置为null
- 闭包