JS內存問題

JS內存問題

來自專欄前端知識點1 人贊了文章

一直以來,對於Js的內存空間這部分的知識概念有些模糊,最近在回顧一些知識點的時候,特地的對js的內存這部分知識加深了一下理解,比如基本類型數據和引用類型數據在js內存中是怎麼回事?什麼是按值傳遞和按引用傳遞?以及對作用域和閉包的理解等等。

1 JavaScript的內存是怎樣的?

JavaScript中有兩種不同數據類型的值,一種是原始值,另外一種是引用類型值,原始值就是常說的基本數據類型值,包括String、Number、Boolean、Undefined和Null這五大基本數據類型,引用值指的是那些可能由多個指構成的對象,包括Object,Function,Array等類型的值。

JavaScript中的內存也分為棧內存和堆內存。一般來說,棧內存中存放的是存儲對象的地址,而堆內存中存放的是存儲對象的具體內容。對於原始類型的值而言,其地址和具體內容都存在與棧內存中;而基於引用類型的值,其地址存在棧內存,其具體內容存在堆內存中。堆內存與棧內存是有區別的,棧內存運行效率比堆內存高,空間相對堆內存來說較小,反之則是堆內存的特點。所以將構造簡單的原始類型值放在棧內存中,將構造複雜的引用類型值放在堆中而不影響棧的效率。

我們看下下面Js的內存示意圖:

var a = 20;var b = abc;var c = true;var d = { m: 20 }

可以看出變數a,b,c屬於原始數據類型的變數,它們的值都存放在棧內存中,而d是一個對象即屬於引用值,棧內存中存放的是它的內存地址,指向堆內存裡面具體的一個值。

接著我們來看下面的一段代碼:

var a = {n:1}; var b = a; a.x = a = {n:2}; console.log(a.x);// --> undefined console.log(b.x);// --> [object Object]

一開始看這道題,然後看答案,一臉懵逼啊有木有???為啥結果輸出的是undefined和[object Object]???

了解了Js的變數在內存的存儲形式之後,我們一起來解釋一下:

1、a是一個引用類型的變數,一開始它在棧內存中的地址是指向堆內存的具體內容{n:1},接著賦值給b,所以b和a一樣,此時都指向對象{n:1};

var a = {n:1} ; var b = {n:1} ;

2、接下來a.x = a = {n:2},我們都知道js的賦值運算是從右往左的,但「.」是優先順序最高的運算符,所以這段代碼先執行了a.x,所以此時對象{n:1}新增加了一個x的屬性,並且值是undefined,所以運行到這裡a和b都指向了對象{n:1,x:undefined};

var a = {n:1,x:undefined} ; var b = {n:1,x:undefined} ;

3、接著,依循「從右往左」的賦值運算順序先執行 a={n:2} ,這時候,a指向的對象發生了改變,變成了新對象{n:2};而a.x = a則是對象{n:1,x:undefined}中的屬性x指向了對象{n:2},所以此時指向的對象變成了{n:1,x:{n:2}}。

var a = {n:2} ; var b = {n:1,x:{n:2}} ;

綜上所述,我們可以看到最後的運行結果,顯然a.x是undefined,b.x是對象{n:2},用對象的字元串形式[object Object]表示。

1.1 Js的內存空間管理

JavaScript的內存分配和回收是自動完成的,滿足一定條件,就會被垃圾回收器自動回收,下面我們簡單的了解下js的內存管理機制。

1.1.1 JavaScript的內存生命周期:

  1. 分配你所需要的內存
  2. 使用分配到的內存(讀、寫)
  3. 不需要時將其釋放、歸還

var num = 10; // 在內存中給數值變數分配空間 alert(num); // 使用內存 num = null; // 使用完畢之後,釋放內存空間 var obj = {v:1}; // 內存中存在{v:1}對象,及obj這個引用地址 obj = {value:2}; // 垃圾回收機制自動清理{v:1},並為新的有用到的{value:2}分配空間

1.1.2 垃圾回收演算法

js垃圾回收有兩種常見的演算法:引用計數和標記清除。

1.1.2.1 引用計數

引用計數就是跟蹤對象被引用的次數,當一個對象的引用計數為0即沒有其他對象引用它時,說明該對象已經無需訪問了,因此就會回收其所佔的內存,這樣,當垃圾回收器下次運行就會釋放引用數為0的對象所佔用的內存。

但引用計數存在一個弊端就是循環引用問題(IE6和IE7就是採用此演算法)。循環引用就是指對象A中包含一個指向對象B的引用,而對象B中也包含一個指向對象的引用。

function problem() { var A = {}; var B = {}; A.a = B; B.a = A;}

上面例子可以看出對象A和B存在循環音引用的問題,即兩個的引用次數均為2,它們在運行之後依然存在,並且引用次數永遠不為0,如果這個函數被多次調用,就有可能引起內存泄漏問題。為了解決循環引用的問題,還有一種方法就是可以實現垃圾回收,那就是標記清除法。

1.1.2.2 標記清除

標記清除法是現代瀏覽器常用的一種垃圾收集方式,當變數進入環境(即在一個函數中聲明一個變數)時,就將此變數標記為「進入環境」,進入環境的變數是不能被釋放,因為只有執行流進入相應的環境,就可能會引用它們。而當變數離開環境時,就標記為「離開環境」。

垃圾收集器在運行時會給儲存在內存中的所有變數加上標記,然後會去掉環境中的變數以及被環境中的變數引用的變數的標記,當執行完畢那些沒有存在引用無法訪問的變數就被加上標記,最後垃圾收集器完成清除工作,釋放掉那些打上標記的變數所佔的內存。

標記清除之所以不存在循環引用的問題,是因為當函數執行完畢之後,對象A和B就已經離開了所在的作用域,此時兩個變數被標記為「離開環境」,等待被垃圾收集器回收,最後釋放其內存。

1.1.3 管理內存

使用具備垃圾收集機制的語言編寫程序,開發人員一般都不必擔心內存管理的問題。但JavaScript在進行內存管理以及垃圾收集時面臨的問題還是有些不同。出於安全方面的考慮,系統分配給瀏覽器的可用內存數量通常要比分配給桌面應用程序的少,防止JavaScript的網頁耗盡全部系統內存而導致系統崩潰。內存限制問題不僅會影響給變數分配內存,同時還會影響調用棧以及在一個線程中能夠同時執行的語句數量。

因此為了確保佔用最少的內存可以讓頁面獲取更好的性能。優化內存佔用的最佳方式就是為執行中的代碼只保存必要的數據。一旦數據不再有用,最好通過將其值設置為null來釋放其引用,即解除引用。這一做法適用於大多全局變數和全局對象的屬性。局部變數會在它們離開執行環境時自動被解除引用。(具體請閱讀《JavaScript高級程序設計》第四章)

可以分析以下代碼:

function createPerson(name){ var localPerson = new Object(); localPerson.name = name; return localPerson; } var globalPerson = createPerson("Junga"); globalPerson = null;//手動解除全局變數的引用

在這個??中,變數globalPerson取得了createPerson()函數的返回的值。在createPerson()的內部創建了一個局部變數localPerson並添加了一個name屬性。由於localPerson在函數執行完畢之後就離開執行環境,因此會自動解除引用,而對於全局變數來說則需要我們手動設置null,解除引用。

不過,解除一個值的引用並不意味著自動回收該值所佔用的內存,解除引用真正的作用是讓值脫離執行環境,以便垃圾收集器下次運行時將其收回。

1.2 內存優化

對於全局變數,JavaScript不能確定它在後面不能夠被用到,所以它會從聲明之後就一直存在於內存中,直至手動釋放或者關閉頁面/瀏覽器,這就導致了某些不必要的內存消耗。我們可以進行以下的優化:

1.2.1 立即執行函數的運用

(function(window, $, undefined) { // 主業務代碼 })(window, jQuery);

立即執行函數的作用就是建立一個獨立的作用域,其一是為了防止全局污染,同時也可以防止過多的定義全局變數造成的內存回收問題。如果你的某些變數真的需要一直存在可以通過上面的方法掛載在window下。同樣,你也可以傳入jQuery進行使用。

1.2.2 手動解除變數的引用

var obj = {a:1,b:2,c:3}; obj = null;

1.2.3 使用回調

除了使用閉包進行內部變數訪問,回調函數也有這個功能。

function getData(callback) { var data = Junga; callback(data); } getData(function(data) { console.log(data); });

回調函數是一種後續傳遞風格(Continuation Passing Style, CPS)的技術,這種風格的程序編寫將函數的業務重點從返回值轉移到回調函數中去。而且其相比閉包的好處也不少:

  1. 如果傳入的參數是基礎類型(如字元串、數值),回調函數中傳入的形參就會是複製值,業務代碼使用完畢以後,更容易被回收;
  2. 通過回調,我們除了可以完成同步的請求外,還可以用在非同步編程中,這也就是現在非常流行的一種編寫風格;
  3. 回調函數自身通常也是臨時的匿名函數,一旦請求函數執行完畢,回調函數自身的引用就會被解除,自身也得到回收。

原文出處:jungahuang.com/2017/02/


推薦閱讀:

苗床里的科技味
iOS12概念修復了iOS11中的問題並增加了大量新功能
超離子冰跟太陽一樣熱!科學家已經在地球上造出
【裝機幫扶站】第180期:萬元神機真的只能用來掃雷!
再看小程序

TAG:內存RAM | 科技 | 計算機科學 |