壹 ❀ 引html
從事計算機相關技術工做的同窗,對於內存空間相關概念多少有所耳聞,畢竟像我這種非計算機科班出身的人,對於棧堆,垃圾回收都能簡單說道幾句;當我明白JS 基本類型與引用類型數據存儲方式不一樣,纔對於爲什麼要使用深拷貝恍然大悟。只是知道和深刻了解是兩碼事,那麼這篇文章從內存空間提及。算法
貳 ❀ 棧、堆與隊列數組
與c語言這種底層語言不一樣,JavaScript並無提供內存管理的接口,而是在建立變量時自動分配內存,當變量再也不須要使用時自動釋放,也就是咱們所常說的垃圾回收機制。瀏覽器
但不論是什麼程序語言,內存的聲明週期都知足如下三個階段:數據結構
a.分配你須要的內存空間閉包
b.使用分配到的內存(讀、寫)dom
c.不須要時將其釋放或歸還函數
大部分語言對於第二步是明確的,但對於JavaScript而言三步都是隱含的,也正是因如此才讓JavaScript開發者產生了不用關心內存管理的錯覺。oop
JavaScript內存空間分爲棧,堆,池,隊列。其中棧存放變量,基本類型數據與指向複雜類型數據的引用指針;堆存放複雜類型數據;池又稱爲常量池,用於存放常量;而隊列在任務隊列也會使用。咱們一一細說。this
1.棧數據結構
棧數據結構具有FILO(first in last out)先進後出的特性,較爲經典的就是乒乓球盒結構,先放進去的乒乓球只能最後取出來。我在 一篇文章看懂JS執行上下文 這篇文章中有提到執行上下文棧,它用於存放js代碼在執行過程當中建立的全部上下文,一樣也具有FILO的特性。
在js中數據類型通常分類基本數據類型(Number Boolean Null Undefined String Symbol)與引用數據類型(Object Array Function ...),其中棧通常用於存放基本類型數據,例如如下代碼在棧內存中分佈:
var a = 1; var b = a; a = 2;
能夠看到基本類型數據的變量名與值都存放在棧內存中,當咱們將變量a複製給b時,棧會新開內存用於存放變量b,且當咱們修改變量a時對變量b不會形成任何影響,由於a與b是互不相關的兩份數據。
2.堆數據結構
堆數據結構是一種無序的樹狀結構,同時它還知足key-value鍵值對的存儲方式;咱們只用知道key名,就能經過key查找到對應的value。比較經典的就是書架存書的例子,咱們知道書名,就能夠找到對應的書籍。
在js中堆內存通常用於存儲引用類型的數據,須要注意的是因爲引用類型的數據通常能夠拓展,數據大小可變,因此存放在堆內存中;但對引用類型數據的引用地址是固定的,因此地址指向仍是會存放在棧內存中。
咱們經過內存圖來模擬如下代碼:
var a = [1,2,3]; var b = a; a.push(4);
當咱們建立數組a時,棧內存中只保存了變量a與指向堆內存中數組的地址指針,而當咱們將a複製給變量b時,其實只是複製了一份地址指針,二者仍是指向同一數組,不管誰修改,都會影響彼此。
這即是咱們熟知的淺拷貝,若想對淺拷貝與深拷貝有更深瞭解,歡迎閱讀博主 深拷貝與淺拷貝的區別,實現深拷貝的幾種方法這篇文章。
3.隊列
隊列具備FIFO(First In First Out)先進先出的特性,與棧內存不一樣的是,棧內存只存在一個出口用於數據進棧出棧;而隊列有一個入口與一個出口,理解隊列一個較爲實際的例子就像咱們排隊取餐,先排隊的永遠能先取到餐。
在js中使用隊列較爲突出的就是js執行機制中的event loop事件循環,若是你們對於js事件執行機制有興趣,能夠閱讀博主 JS執行機制詳解,定時器時間間隔的真正含義 這篇文章,必定會讓你有所收穫。
叄 ❀ 垃圾回收機制
咱們在前面已經說到JS內存分配回收由計算機自動完成,同時也提到了垃圾回收機制這個概念,這裏來細說。
1.js中的內存回收
在js中,垃圾回收器每隔一段時間就會找出那些再也不使用的數據,並釋放其所佔用的內存空間。
以全局變量和局部變量來講,函數中的局部變量在函數執行結束後這些變量已經再也不被須要,因此垃圾回收器會識別並釋放它們。而對於全局變量,垃圾回收器很難判斷這些變量何時纔不被須要,因此儘可能少使用全局變量。
2.垃圾回收的兩種模式
那麼垃圾回收器是如何檢測變量是否須要的呢,大致上分爲兩種檢測手段,引用計數與標記清除。
引用計數
引用計數的判斷原理很簡單,就是看一份數據是否還有指向它的引用,若是沒有任何對象再指向它,那麼垃圾回收器就會回收,舉個例子:
// 建立一個對象,由變量o指向這個對象的兩個屬性 var o = { name: '聽風是風', handsome: true }; // name雖然設置爲了null,但o依舊有name屬性的引用 o.name = null; var s = o; // 咱們修改並釋放了o對於對象的引用,但變量s依舊存在引用 o = null; // 變量s也再也不引用,對象很快會被垃圾回收器釋放 s = null;
引用計數存在一個很大的問題,就是對象間的循環引用,好比以下代碼中,對象o1與o2相互引用,即使函數執行完畢,垃圾回收器經過引用計數也沒法釋放它們。
function f() { var o1 = {}; var o2 = {}; o1.a = o2; // o1 引用 o2 o2.a = o1; // o2 引用 o1 return; }; f();
標記清除
標記清除的概念也好理解,從根部出發看是否能達到某個對象,若是能達到則認定這個對象還被須要,若是沒法達到,則釋放它,這個過程大體分爲三步:
a.垃圾回收器建立roots列表,roots一般是代碼中保留引用的全局變量,在js中,咱們通常認定全局對象window做爲root,也就是所謂的根部。
b.從根部出發檢查全部 的roots,全部的children也會被遞歸檢查,能從root到達的都會被標記爲active。
c.未被標記爲active的數據被認定爲再也不須要,垃圾回收器開始釋放它們。
當一個對象零引用時,咱們從根部必定沒法到達;但反過來,從根部沒法到達的不必定是嚴格意義上的零引用,好比循環引用,因此標記清除要更優於引用計數。
從2012年起,全部現代瀏覽器都使用了標記清除垃圾回收算法,但老版本的IE6除外。
肆 ❀ 如何避免內存泄漏
咱們已經知道了垃圾回收的原理,那麼咱們如何避免建立沒法回收的對象,以致形成內存泄漏的尷尬呢?下面說說常見的四種js內存泄漏。
1.全局變量
儘量少的去建立全局變量是js開發者的常識,但以下兩種方式仍是會意外的建立全局變量,第一是在函數中聲明變量未使用var:
function fn() { a = 1; }; fn(); window.a //1
上述代碼中咱們在函數體內聲明瞭一個變量a,因爲未使用var聲明,即使在函數體內,但它依舊是一個全局變量。咱們知道全局變量等同於在window上添加屬性,因此在函數執行完畢,咱們依舊能夠訪問到它。
第二種是在函數體內經過this來建立變量:
function fn() { this.a = 1; }; fn(); window.a //1
咱們知道,當直接調用函數fn時,等同於window.fn(),因此函數體內的this會指向window,因此本質上仍是建立了一個全局變量。
固然上述問題也不是沒法解決,咱們可使用嚴格模式來避免這個問題,試着在代碼頭部添加‘use strict’,你會發現a就沒法訪問了,由於嚴格模式下,全局對象指向undefined。
有時候咱們沒法避免使用全局變量,那麼記得在使用完畢後手動釋放它們,例如讓變量指向null。
2.被遺忘的定時器或回調函數
var serverData = loadData(); setInterval(function () { var renderer = document.getElementById('renderer'); if (renderer) { renderer.innerHTML = JSON.stringify(serverData); } }, 3000);
在上述代碼中,當dom元素renderer被移除時,因爲是週期定時器的緣故,定時器回調函數始終沒法被回收,這也致使了定時器會一直對數據serverData保持引用,好的作法是在不須要時中止定時器。
在例如咱們在使用事件監聽時,若是再也不須要監聽記得移除監聽事件。
var element = document.getElementById('button'); function onclick(event) { element.innerHTML = 'text'; }; element.addEventListener('click', onclick); // 移除監聽 element.removeEventListener('click', onclick);
3.閉包
閉包在js開發中是極其常見的,咱們來看個例子:
var theThing = null; var replaceThing = function () { var originalThing = theThing; var unused = function () { //unused未執行,但一直保持對theThing的引用 if (originalThing) console.log("hi"); }; //建立一個新對象 theThing = { longStr: new Array(1000000).join('*'), someMethod: function () { console.log("message"); } }; }; setInterval(replaceThing, 1000);
定時器每次調用replaceThing,theThing都會得到一個包含數組longStr與閉包someMethod的新對象。
閉包unused保持着對象originalThing的引用,由於theThing賦值的緣故,也保持了對theThing的引用。雖然unused沒執行,但引用關係會致使originalThing一直沒法被回收,那麼theThing也同樣。正確作法是在replaceThing 最後添加originalThing = null;
因此咱們常說,對於閉包中的變量,在不須要時必定記得手動釋放。
4.DOM的引用
操做dom老是被認爲是很差的,但必定得操做,咱們的習慣是經過一個變量來存儲它,這樣就能夠反覆使用了,但這也會形成一個問題,dom會被引用2次。
var elements = document.getElementById('button') function doStuff() { elements.innerHTML = '聽風是風'; }; // 清除引用 elements = null; document.body.removeChild(document.getElementById('button'));
在上述代碼中,一次引用是基於dom樹的引用,第二是變量elements的引用,當咱們不須要這個dom時,都作兩次清除操做。
伍 ❀ 參考