js内存管理机制
js在创建变量(对象、字符串等)时自动进行了分配内存,并且在不使用它们时自动释放。释放的过程称为垃圾回收。
- 自动是混乱的根源,并让
js开发者错误的感觉它们可以不关心内存管理。
js内存管理机制和内存的生命周期是一一对应的,首先需要分配内存,然后使用内存,最后释放内存。
js语言不需要程序员手动分配内存,绝大多数情况下也不需要手动释放内存,对js程序员来说通常就是使用内存(即使用变量、函数、对象等)
内存分配
let flag = true;
let str = 'hello';
let arr = [2, null, 'aa'];
function fn (a) {
return a;
}
内存使用
- 使用值的过程实际上是对分配内存进行读取与写入的操作。
- 读取与写入可能是:写入一个变量或者一个对象的属性值,甚至传递函数的参数。
str = 'byebye';
fn(str);
内存回收
- 垃圾内存回收
GC(Garbage Collection)
- 内存泄漏一般都是发生在这一步,
js的内存回收机制虽然能回收绝大部分的垃圾内存,但是还是存在回收不了的情况,如果存在这种情况,需要手动清理内存。
js垃圾回收的两种方式
引用计数垃圾收集
- 把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。
- 如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
let a = {
a: 1,
b: 2
}
let b = a;
a = 1;
- 当前执行环境中,对象字面量的内存还没有被回收,需要手动释放内存。
b = null;
标记清除法
- 当变量进入执行环境时标记为“进入环境”,当变量离开执行环境时则标记为“离开环境”,被标记为“进入环境”的变量是不能被回收的,因为它们正在被使用,而标记为“离开环境”的变量则是可以被回收。
- 环境可以理解为作用域,但是全局作用域的变量只会在页面关闭才会销毁。
var b = 100;
function add () {
var a = 1;
return a + b;
}
add();
js内存泄露场景
- 下面例子:是在执行环境中,没离开当前执行环境,还触发标记清除法。
意外的全局变量
function count(number) {
basicCount = 2;
return basicCount + number;
}
- 现在已经能够避免这种错误,
eslint会直接报错。
被遗忘的计时器
<template>
<div></div>
</template>
<script>
export default {
methods: {
refresh() {
},
},
mounted() {
setInterval(function() {
this.refresh()
}, 2000)
},
}
</script>
- 上面的组件销毁的时候,
setInterval还是在运行的,里面涉及到的内存是没法回收的(浏览器会认为这是必须 的内存,不是垃圾内存),需要在组件销毁的时候清除计时器,如下:
<template>
<div></div>
</template>
<script>
export default {
methods: {
refresh() {
},
},
mounted() {
this.refreshInterval = setInterval(function() {
this.refresh()
}, 2000)
},
beforeDestroy() {
clearInterval(this.refreshInterval)
},
}
</script>
被遗忘的事件监听器
<template>
<div></div>
</template>
<script>
export default {
mounted() {
window.addEventListener('resize', () => {
})
},
}
</script>
- 上面的组件销毁的时候,
resize事件还是在监听中,里面涉及到的内存都是没法回收的(浏览器会认为这是必须的内存,不是垃圾内存),需要在组件销毁的时候移除相关的事件,如下:
<template>
<div></div>
</template>
<script>
export default {
mounted() {
this.resizeEventCallback = () => {
}
window.addEventListener('resize', this.resizeEventCallback)
},
beforeDestroy() {
window.removeEventListener('resize', this.resizeEventCallback)
},
}
</script>
被遗忘的ES6 Set\Map成员
let map = new Set();
let value = { test: 22};
map.add(value);
value= null;
let map = new Set();
let value = { test: 22};
map.add(value);
map.delete(value);
value = null;
被遗忘的订阅发布事件监听器
<template>
<div @click="onClick"></div>
</template>
<script>
import customEvent from 'event'
export default {
methods: {
onClick() {
customEvent.emit('test', { type: 'click' })
},
},
mounted() {
customEvent.on('test', data => {
console.log(data)
})
},
beforeDestroy() {
customEvent.off('test')
},
}
</script>
被遗忘的闭包
function closure() {
const name = 'xianshannan'
return () => {
return name
.split('')
.reverse()
.join('')
}
}
const reverseName = closure()
- 在当前执行环境未结束的情况下,严格来说,这样是有内存泄漏的,
name 变量是被 closure 返回的函数调用了,但是返回的函数没被使用,这个场景下 name 就属于垃圾内存。name 不是必须的,但是还是占用了内存,也不可被回收。
脱离DOM的引用
- 每个页面上的
DOM 都是占用内存的,假设有一个页面 A 元素,我们获取到了 A 元素 DOM 对象,然后赋值到了一个变量(内存指向是一样的),然后移除了页面的 A 元素,如果这个变量由于其他原因没有被回收,那么就存在内存泄漏,如下:
class Test {
constructor() {
this.elements = {
button: document.querySelector('#button'),
div: document.querySelector('#div'),
span: document.querySelector('#span'),
}
}
removeButton() {
document.body.removeChild(this.elements.button)
}
}
const a = new Test()
a.removeButton()
- 上面的例子
button 元素 虽然在页面上移除了,但是内存指向换为了 this.elements.button,内存占用还是存在的。所以上面的代码还需要这样写: this.elements.button = null,手动释放这个内存。
附录