也許很多人像我一樣,覺得JS有垃圾回收機制,內存就可以不管了,以至於在全局作用域下定義了很多變量,自以為JS會自動回收,直到最近,看了阮一峰老師,關於javascript內存泄漏的文章時,才發現自己寫的代碼,存在很嚴重的內存泄漏問題,再者,因為忽略對內存的學習,導致后面很多進階概念很模糊,比如深復制與淺復制的區別,比如閉包、作用域鏈等等。
堆與棧
與C/C++不同,JavaScript語言沒有嚴格意義上,區分堆與棧,所以我們可以理解為,JavaScript所有的數據都是存放在堆內存
中。
不過,在某些場景下,我們仍然需要借助堆棧數據結構來處理,所以有必要理解一下這兩個數據結構的區別。
棧:也叫做堆棧
棧數據結構的一個特點就是后進先出
,好比羽毛球盒子,在一頭放羽毛球,在另外一頭取羽毛球。
堆數據結構,好比書架上的書,雖然已經按順序放好了,但是我們只要知道書的名字,就可以對應的取下來,類似於JSON對象中的key-value
。
變量對象與基本數據類型
JavaScript中的數據類型大致分為,基本數據類型
與引用數據類型
,上文提到,JavaScript中所有的數據都是存放在堆內存
中,但是,這里提到的變量對象
(在執行上下文創建階段生成),由於它有特殊的職能,所以在理解上就把它與堆內存單獨分開了,如下圖所示:
一般變量對象
里面存放的是基本數據類型
,包括Undefined、Null、Boolean、Number、String
,它們到是按值訪問
的。
demo01
var a = 20;
var b = a;
b = 30;
console.log(a);//20
上面這段代碼指的是,在變量對象中執行數據復制的時候,其實系統會自動為新的變量分配一個新的值,所以a與b其實已經是完全獨立的兩個變量,只是值一樣而已。
堆內存與引用數據類型
javascript中的引用數據類型
是存放在堆內存
中的,但是不同於變量對象
,javascript是不允許直接訪問堆內存中的數據,所以如果我們要訪問引用數據類型的時候,采用的是按引用訪問
,其實就是在變量對象
中存放了一個指向對象的句柄,可以理解為一個地址,要訪問堆內存中的對象,就要通過這個引用句柄來訪問,例如上圖中的d變量,就是一個指向對象的地址。
demo02
var m = { a:10,b:20};
var n = m;
n.a = 15;
console.log(m.a);//15
上面這段代碼指的是執行引用類型數據的復制時,在變量對象中會分配一個新的值,來存放新的變量,但是這兩個變量的地址是一樣的,相當於指向的對象是一樣的,所以各自改變對象里面的屬性值,會互相影響,如下圖
內存泄漏
上面講解了JavaScript中的內存空間,接下來就要講解,我寫這篇文章的初衷,就是我代碼中嚴重的內存泄漏
內存泄漏:就是不再用到的內存,但是沒有及時釋放,就叫做內存泄漏
有些語言必須手動釋放內存,程序員負責內存的管理,例如C語言
char *buffer;
buffer = (char*) malloc(42);
//do something with buffer
free(buffer);
這里malloc
就是負責分配內存,free
是負責釋放內存。
那么JavaScript中的垃圾回收機制又是怎么一回事呢?
垃圾回收機制
以前我一直天真的以為,垃圾回收機制就像人工智能一樣,會自動幫你識別出不用的內存,然后釋放掉,然而真相只有一個
垃圾回收機制的原理就是,使用引用計數
法,就是語言引擎有一張“引用表”,保存了內存里面所有的資源的引用次數,就像下面這樣
但是如果一個值不再需要了,引用數卻不為0,垃圾回收機制是無法釋放這塊內存,從而導致``內存泄漏```
例如:
const arr = [1,2,3,4,5];
console.log('hello world");
arr的引用次數為1,盡管后面不再使用arr了,但是它還會持續占用內存,所以一般要這樣處理
const arr = [1,2,3,4,5];
console.log('hello world");
arr = null;
讓arr的指向為空,垃圾回收機制就會默認它的引用數為0而回收掉。
避免內存泄漏
在局部作用域中,等函數執行完畢,變量就沒有存在的必要了,js垃圾回收機制很快做出判斷並且回收,但是對於全局變量,很難判斷什么時候不用,所以,經驗之談就是,盡量少使用全局變量。
我們在使用閉包的時候,就會造成嚴重的內存泄漏,因為閉包的原因,局部變量會一直保存在內存中,所以在使用閉包的時候,要多加小心。