更新:謝謝你們的支持,最近折騰了一個博客官網出來,方便你們系統閱讀,後續會有更多內容和更多優化,猛戳這裏查看前端
------ 如下是正文 ------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。
我是木易楊,網易高級前端工程師,跟着我每週重點攻克一個前端面試重難點。接下來讓我帶你走進高級前端的世界,在進階的路上,共勉!