垃圾回收机制
原理:
垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。
垃圾回收有两种方法:
- 标记清除
- 引用计数
标记清除:在函数申明一个变量的时候,就将这个变量标记为‘进入环境’,而当变量离开环境时,则将其标记为‘离开环境’。这些被标记为‘离开环境’的变量就是准备删除的变量,然后垃圾回收器就会销毁那些带标记的值并释放他们所占用的内存。
function
test(){
var a = 10 ; //被标记 ,进入环境
var b = 20 ; //被标记 ,进入环境
}
test(); //执行完毕 之后 a、b又被标离开环境,被回收。
var m = 0,n = 19 // 把 m,n,add() 标记为进入环境。
add(m, n) // 把 a, b, c标记为进入环境。
console.log(n) // a,b,c标记为离开环境,等待垃圾回收。
function add(a, b) {
a++
var c = a + b
return c
}
引用计数:引用计数的含义是跟踪记录每个值被引用的的次数,当申明一个变量,并将一个引用类型的值赋值给变量时,则这个值的引用次数就是1,当同一个值又被赋值给另外一个变量,则该值的引用次数加一。相反如果包含对这个值引用的变量又取得另外一个值,则这个值的引用次数减一。当这个值的引用次数变为o的时候,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。
这样,当垃圾回收器下次再运行时,它就会释放那些引用次数为0的值所占用的内存。
但是很重要的一点是当遇到循环引用的时候,函数的引用次数就不会为0,所以不会被垃圾回收器回收内存,会造成内存泄露。在IE中涉及COM对象,就会存在循环引用的问题。
function
test(){
var a = {} ; //a的引用次数为0
var b = a ; //a的引用次数加1,为1
var c =a; //a的引用次数再加1,为2
var b ={}; //a的引用次数减1,为1
}
内存泄漏
内存泄漏指任何对象在您不再拥有或需要它之后仍然存在。
垃圾回收器定期扫描对象,并计算引用了每个对象的其他对象的数量。如果一个对象的引用数量为 0(没有其他对象引用过该对象),或对该对象的惟一引用是循环的,那么该对象的内存即可回收。
虽然JavaScript会自动垃圾收集,但是如果我们的代码写法不当,会让变量一直处于“进入环境”的状态,无法被回收。下面列一下内存泄漏常见的几种情况:
1. 意外的全局变量
function foo(arg) {
bar = "this is a hidden global variable";
}
ar没被声明,会变成一个全局变量,在页面关闭之前不会被释放。
另一种意外的全局变量可能由this
创建:
function foo() {
this.variable = "potential accidental global";
}
// foo 调用自己,this 指向了全局对象(window)
foo();
在 JavaScript 文件头部加上 'use strict',可以避免此类错误发生。启用严格模式解析 JavaScript ,避免意外的全局变量。
2. 被遗忘的计时器或回调函数
var someResource = getData();
setInterval(function() {
var node = document.getElementById('Node');
if(node) {
// 处理 node 和 someResource
node.innerHTML = JSON.stringify(someResource));
}
}, 1000);
这样的代码很常见,如果id为Node的元素从DOM中移除,该定时器仍会存在,同时,因为回调函数中包含对someResource的引用,定时器外面的someResource也不会被释放。
3. 闭包
function bindEvent(){
var obj=document.createElement('xxx')
obj.onclick=function(){
// Even if it is a empty function
}
}
闭包可以维持函数内局部变量,使其得不到释放。上例定义事件回调时,由于是函数内定义函数,并且内部函数--事件回调引用外部函数,形成了闭包。
// 将事件处理函数定义在外面
function bindEvent() {
var obj = document.createElement('xxx')
obj.onclick = onclickHandler
}
// 或者在定义事件处理函数的外部函数中,删除对dom的引用
function bindEvent() {
var obj = document.createElement('xxx')
obj.onclick = function() {
// Even if it is a empty function
}
obj = null
}
解决之道,将事件处理函数定义在外部,解除闭包,或者在定义事件处理函数的外部函数中,删除对dom的引用。
4. 没有清理的DOM元素引用
有时,保存 DOM 节点内部数据结构很有用。假如你想快速更新表格的几行内容,把每一行 DOM 存成字典(JSON 键值对)或者数组很有意义。此时,同样的 DOM 元素存在两个引用:一个在 DOM 树中,另一个在字典中。将来你决定删除这些行时,需要把两个引用都清除。
var elements = {
button: document.getElementById('button'),
image: document.getElementById('image'),
text: document.getElementById('text')
};
function doStuff() {
image.src = 'http://some.url/image';
button.click();
console.log(text.innerHTML);
}
function removeButton() {
document.body.removeChild(document.getElementById('button'));
// 此时,仍旧存在一个全局的 #button 的引用
// elements 字典。button 元素仍旧在内存中,不能被 GC 回收。
}
虽然我们用removeChild移除了button,但是还在elements对象里保存着#button的引用,换言之,DOM元素还在内存里面
垃圾回收的使用场景优化
数组array优化
将[]赋值给一个数组对象,是清空数组的捷径(例如: arr = [];),但是需要注意的是,这种方式又创建了一个新的空对象,并且将原来的数组对象变成了一小片内存垃圾!实际上,将数组长度赋值为0(arr.length = 0)也能达到清空数组的目的,并且同时能实现数组重用,减少内存垃圾的产生。
const arr = [1, 2, 3, 4];
console.log('浪里行舟');
arr.length = 0 // 可以直接让数字清空,而且数组类型不变。
// arr = []; 虽然让a变量成一个空数组,但是在堆上重新申请了一个空数组对象。
对象尽量复用
var t = {} // 每次循环都会创建一个新对象。
for (var i = 0; i < 10; i++) {
// var t = {};// 每次循环都会创建一个新对象。
t.age = 19
t.name = '123'
t.index = i
console.log(t)
}
t = null //对象如果已经不用了,那就立即设置为null;等待垃圾回收。
在循环中的函数表达式,能复用最好放到循环外面。
// 在循环中最好也别使用函数表达式。
for (var k = 0; k < 10; k++) {
var t = function(a) {
// 创建了10次 函数对象。
console.log(a)
}
t(k)
}
// 推荐用法
function t(a) {
console.log(a)
}
for (var k = 0; k < 10; k++) {
t(k)
}
t = null
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。