更新:谢谢你们的支持,最近折腾了一个博客官网出来,方便你们系统阅读,后续会有更多内容和更多优化,猛戳这里查看前端
------ 如下是正文 ------webpack
本期的主题是调用堆栈,本计划一共28期,每期重点攻克一个面试重难点,若是你还不了解本进阶计划,文末点击查看所有文章。git
若是以为本系列不错,欢迎点赞、评论、转发,您的支持就是我坚持的最大动力。github
JS内存空间分为栈(stack)、堆(heap)、池(通常也会归类为栈中)。 其中栈存放变量,堆存放复杂对象,池存放常量,因此也叫常量池。web
昨天文章介绍了堆和栈,小结一下:面试
栈
内存(不包含闭包中的变量)堆
内存今日补充一个知识点,就是闭包中的变量并不保存中栈内存中,而是保存在堆内存
中,这也就解释了函数以后以后为何闭包还能引用到函数内的变量。算法
function A() {
let a = 1
function B() {
console.log(a)
}
return B
}
复制代码
闭包
的简单定义是:函数 A 返回了一个函数 B,而且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。segmentfault
函数 A 弹出调用栈后,函数 A 中的变量这时候是存储在堆上的,因此函数B依旧能引用到函数A中的变量。如今的 JS 引擎能够经过逃逸分析辨别出哪些变量须要存储在堆上,哪些须要存储在栈上。跨域
闭包的介绍点到为止,【进阶2期】 做用域闭包会详细介绍,敬请期待。浏览器
今天文章的重点是内存回收和内存泄漏。
JavaScript有自动垃圾收集机制,垃圾收集器会每隔一段时间就执行一次释放操做,找出那些再也不继续使用的值,而后释放其占用的内存。
对垃圾回收算法来讲,核心思想就是如何判断内存已经再也不使用,经常使用垃圾回收算法有下面两种。
引用计数算法定义“内存再也不使用”的标准很简单,就是看一个对象是否有指向它的引用。若是没有其余对象指向它了,说明该对象已经再也不须要了。
// 建立一个对象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对象已经没有引用,很快会被回收
复制代码
引用计数有一个致命的问题,那就是循环引用
若是两个对象相互引用,尽管他们已再也不使用,可是垃圾回收器不会进行回收,最终可能会致使内存泄露。
function cycle() {
var o1 = {};
var o2 = {};
o1.a = o2;
o2.a = o1;
return "cycle reference!"
}
cycle();
复制代码
cycle
函数执行完成以后,对象o1
和o2
实际上已经再也不须要了,但根据引用计数的原则,他们之间的相互引用依然存在,所以这部份内存不会被回收。因此现代浏览器再也不使用这个算法。
可是IE依旧使用。
var div = document.createElement("div");
div.onclick = function() {
console.log("click");
};
复制代码
上面的写法很常见,可是上面的例子就是一个循环引用。
变量div有事件处理函数的引用,同时事件处理函数也有div的引用,由于div变量可在函数内被访问,因此循环引用就出现了。
标记清除算法将“再也不使用的对象”定义为“没法到达的对象”。即从根部(在JS中就是全局对象)出发定时扫描内存中的对象,凡是能从根部到达的对象,保留。那些从根部出发没法触及到的对象被标记为再也不使用,稍后进行回收。
没法触及的对象包含了没有引用的对象这个概念,但反之未必成立。
因此上面的例子就能够正确被垃圾回收处理了。
因此如今对于主流浏览器来讲,只须要切断须要回收的对象与根部的联系。最多见的内存泄露通常都与DOM元素绑定有关:
email.message = document.createElement(“div”);
displayList.appendChild(email.message);
// 稍后从displayList中清除DOM元素
displayList.removeAllChildren();
复制代码
上面代码中,div
元素已经从DOM树中清除,可是该div
元素还绑定在email对象中,因此若是email对象存在,那么该div
元素就会一直保存在内存中。
对于持续运行的服务进程(daemon),必须及时释放再也不用到的内存。不然,内存占用愈来愈高,轻则影响系统性能,重则致使进程崩溃。 对于再也不用到的内存,没有及时释放,就叫作内存泄漏(memory leak)
使用 Node
提供的 process.memoryUsage
方法。
console.log(process.memoryUsage());
// 输出
{
rss: 27709440, // resident set size,全部内存占用,包括指令区和堆栈
heapTotal: 5685248, // "堆"占用的内存,包括用到的和没用到的
heapUsed: 3449392, // 用到的堆的部分
external: 8772 // V8 引擎内部的 C++ 对象占用的内存
}
复制代码
判断内存泄漏,以heapUsed
字段为准。
详细的JS内存分析将在【进阶20期】性能优化详细介绍,敬请期待。
ES6 新出的两种数据结构:WeakSet
和 WeakMap
,表示这是弱引用,它们对于值的引用都是不计入垃圾回收机制的。
const wm = new WeakMap();
const element = document.getElementById('example');
wm.set(element, 'some information');
wm.get(element) // "some information"
复制代码
先新建一个 Weakmap
实例,而后将一个 DOM 节点做为键名存入该实例,并将一些附加信息做为键值,一块儿存放在 WeakMap
里面。这时,WeakMap
里面对element的引用就是弱引用,不会被计入垃圾回收机制。
昨天文章留了一道思考题,群里讨论很热烈,你们应该都知道原理了,如今来简单解答下。
var a = {n: 1};
var b = a;
a.x = a = {n: 2};
a.x // --> undefined
b.x // --> {n: 2}
复制代码
答案已经写上面了,这道题的关键在于
.
的优先级高于=
,因此先执行a.x
,堆内存中的{n: 1}
就会变成{n: 1, x: undefined}
,改变以后相应的b.x
也变化了,由于指向的是同一个对象。从右到左
,因此先执行a = {n: 2}
,a
的引用就被改变了,而后这个返回值又赋值给了a.x
,须要注意的是这时候a.x
是第一步中的{n: 1, x: undefined}
那个对象,其实就是b.x
,至关于b.x = {n: 2}
问题一:
从内存来看 null 和 undefined 本质的区别是什么?
问题二:
ES6语法中的 const 声明一个只读的常量,那为何下面能够修改const的值?
const foo = {};
foo = {}; // TypeError: "foo" is read-only
foo.prop = 123;
foo.prop // 123
复制代码
问题三:
哪些状况下容易产生内存泄漏?
进阶系列文章汇总:github.com/yygmind/blo…,内有优质前端资料,欢迎领取,以为不错点个star。
我是木易杨,网易高级前端工程师,跟着我每周重点攻克一个前端面试重难点。接下来让我带你走进高级前端的世界,在进阶的路上,共勉!