高頻面試題:通過【垃圾回收機(jī)制】的角度認(rèn)識(shí)【Map與WeakMap】的區(qū)別
原文鏈接: https://www.zhihu.com/people/yiqun-a-qia
前言:前段時(shí)間剛接觸WeakMap時(shí)概念很模糊,查了很多文章大部分是簡(jiǎn)單掠過,后來通過學(xué)習(xí)了垃圾回收機(jī)制后有感而發(fā)。
簡(jiǎn)單介紹JavaScript中的垃圾回收
JavaScript是使用垃圾回收的語言,也就是說執(zhí)行環(huán)境負(fù)責(zé)在代碼執(zhí)行時(shí)管理內(nèi)存。
眾所周知,我們一般使用 javascript 進(jìn)行開始都很少關(guān)注垃圾回收是如何進(jìn)行的,
對(duì)于開發(fā)者來說,JavaScript 的內(nèi)存管理是自動(dòng)的、無形的。
于是乎對(duì)于學(xué)習(xí)WeakMap這個(gè)概念需要用到垃圾回收相關(guān)知識(shí),
先簡(jiǎn)單描述下垃圾回收是如何進(jìn)行。
例子:“我有一個(gè)朋友”
沒有什么是用例子解決不了的
// 從前,我有一個(gè)朋友。
let myFriend = {
info: "I have a friend",
attr: "Lsp"
}

這里我們?cè)谌挚臻g聲明了一位“朋友”?myFriend

他的引用地址指向了{ info: "I have a friend", attr: "Lsp" }
myFriend = null;
// 此時(shí)我們已經(jīng)沒有這位朋友了

由于我們與這位“朋友”斷了聯(lián)系,
從?根(window/global)?開始 查詢 找不到 其引用,此時(shí)垃圾回收機(jī)制會(huì)把它當(dāng)作垃圾進(jìn)行自動(dòng)回收,并釋放內(nèi)存。
多個(gè)引用
現(xiàn)在我們?cè)倥e一個(gè)例子,聲明多個(gè)變量指向同一個(gè)引用
// 大家好,我是...
let me = null;
// 然后,我有一個(gè)朋友他...
let myFriend = {
info: "I have a friend",
attr: "Lsp"
}
// (....?)
me = myFriend;
你說的這個(gè)朋友是不是你.jpg

由此可見目前有兩個(gè)變量引用地址指向了同一塊地方?{ info: "I have a friend", attr: "Lsp" }。
我們現(xiàn)在將其中一個(gè)變量進(jìn)行斷開。
// 我說的那位朋友真的不是我!
me = null;

即使將其中一個(gè)變量的引用地址斷開
另外一個(gè) "朋友"變量 任然繼續(xù)引用
(也就是說對(duì)象還是可以通過根查找到)
所以垃圾回收機(jī)制不會(huì)將它進(jìn)行回收。
回收策略
JavaScript最常用垃圾回收策略是"標(biāo)記清理(mark-and-sweep)"
策略的大意即為:
遍歷空間下所有的對(duì)象,并標(biāo)記活著的,有被引用的并且最終可以到達(dá)根(window/global)的對(duì)象。
在垃圾回收階段的時(shí)候,將沒有標(biāo)記進(jìn)行清除。
這里回收策略不是本章重點(diǎn),具體策略在底層代碼上還會(huì)再細(xì)分,以及內(nèi)存分代對(duì)應(yīng)具體算法,暫時(shí)不展開講,有興趣可以查閱樸靈《深入淺出Node.js》以及V8垃圾回收機(jī)制相關(guān)文章。
JavaScript中Map與WeakMap
上面贅述那么多終于來講本文關(guān)鍵了,
提前講上文原因在于WeakMap?的特點(diǎn)與垃圾回收機(jī)制有關(guān)。
我們引用一下《JavaScript高級(jí)程序設(shè)計(jì)(第四版)》的原話。
ECMAScript6新增的”弱映射“(WeakMap)是一種新的集合類型,為這門語言帶來了增強(qiáng)的鍵值對(duì)存儲(chǔ)機(jī)制。WeakMap是Map的”兄弟“類型,其API也是Map的子集。WeakMap中的”weak“(弱),描述的是JavaScript垃圾回收程序?qū)Υ摹比跤成洹爸墟I的方式。---- 《JavaScript高級(jí)程序設(shè)計(jì)(第四版)》6.5
嗯,說得好棒很詳細(xì)的樣子!
可是我完全不懂呢( 嗯嗯嗯我完全理解了呢.jpg
Map與WeakMap簡(jiǎn)單區(qū)別
Map的鍵值可以是原始數(shù)據(jù)類型和引用類型,WeakMap的鍵值只能說引用類型(object)
Map可以迭代遍歷鍵,WeakMap不可迭代遍歷鍵
WeakMap中的”weak“表示弱映射的鍵是”弱弱地拿著“的,意思就是,這些鍵不屬于正式的引用。
換言之,WeakMap所構(gòu)建的實(shí)例中,
其key鍵所對(duì)應(yīng)引用地址的引用斷開或不屬于指向同一個(gè)內(nèi)存地址的時(shí)候,
其對(duì)應(yīng)value值就會(huì)被加入垃圾回收隊(duì)伍。
(粗暴理解為:因?yàn)閗ey必須是個(gè)引用類型,當(dāng)key引用斷了或變了,這個(gè)鍵值對(duì)就可以進(jìn)垃圾桶了)
觀察內(nèi)存空間理解WeakMap
因?yàn)橥ǔl件下很難察覺WeakMap里面keyValue什么時(shí)候消失
但是通過某一個(gè)引用類型的值大到足夠占據(jù)一定內(nèi)存時(shí)候
我們可以通過觀察內(nèi)存的變化來觀察WeakMap的特性
示例使用的Node.js的進(jìn)程Api?process.memoryUsage()配合手動(dòng)垃圾回收global.gc()在終端觀察,
也可以使用Chrome瀏覽器Performance功能錄制內(nèi)存變化,
但是為了方便用代碼展示就依次展開了,0 .0 本質(zhì)觀察到內(nèi)存變化即可,手段可以有多種。
glabal.gc()
手動(dòng)調(diào)用一次垃圾回收。需要在運(yùn)行js文件時(shí)候增加命令 --expose-gc,一般環(huán)境下不推薦使用,這里做學(xué)習(xí)用。
process.memoryUsage()
查看Node進(jìn)程的內(nèi)存占用情況。
返回值為對(duì)象其中包含五個(gè)屬性 rss,heapTotal,heapUsed,external,arrayBuffers;
其中主要屬性是?heapTotal和heapUsed對(duì)應(yīng)的是V8的堆內(nèi)存信息。
heapTotal是堆中總共申請(qǐng)的內(nèi)存量,heapUsed表示目前堆中使用的內(nèi)存量。單位都為字節(jié)。
現(xiàn)在我們通過代碼來展示
// index.js
// 第一次手動(dòng)清理垃圾以確保為最新狀態(tài),觀察內(nèi)存情況
global.gc();
console.log(`第一次垃圾回收,當(dāng)前內(nèi)存使用情況:${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)}MB`);
const wm = new WeakMap();
let key = {};
// 給 WeakMap實(shí)例 賦值一個(gè) 占領(lǐng)內(nèi)存足夠大的 鍵值對(duì)
wm.set(key, new Array(114514 * 19));
// 手動(dòng)清理一下垃圾 觀察內(nèi)存占用情況
global.gc();
console.log(`第二次垃圾回收,當(dāng)前內(nèi)存使用情況:${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)}MB`);
// 此時(shí)把 key鍵 的引用進(jìn)行斷開,并觀察內(nèi)存占用情況
key = null;
// key = new Array();
// 這種改變引用地址寫法也可以引起 弱映射,因?yàn)橐玫刂凡辉偈峭瑝K內(nèi)存地址 WeakMap內(nèi)對(duì)應(yīng)的value也會(huì)被垃圾回收
global.gc();
console.log(`第三次垃圾回收,當(dāng)前內(nèi)存使用情況:${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)}MB`);
$ node --expose-gc index.js第一次垃圾回收,當(dāng)前內(nèi)存使用情況:1.66MB
第二次垃圾回收,當(dāng)前內(nèi)存使用情況:18.45MB
第三次垃圾回收,當(dāng)前內(nèi)存使用情況:1.84MB
那么我們來看看Map的情況,
與上方index.js的代碼一致,把new WeakMap()換成new Map()。
看看終端輸出效果
$ node --expose-gc index.js
第一次垃圾回收,當(dāng)前內(nèi)存使用情況:1.66MB
第二次垃圾回收,當(dāng)前內(nèi)存使用情況:18.45MB
第三次垃圾回收,當(dāng)前內(nèi)存使用情況:18.44MB很明顯我們將key = null的引用地址斷開后 ,
value?仍然存在Map所構(gòu)建的實(shí)例里面,一如既往還在內(nèi)存里面。
現(xiàn)在我們將代碼場(chǎng)景改成Map的樣子
// index.js
// 第一次手動(dòng)清理垃圾以確保為最新狀態(tài),觀察內(nèi)存情況
global.gc();
console.log(
`第一次垃圾回收,當(dāng)前內(nèi)存使用情況:${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)}MB`
);
const m = new Map();
let key = {};
m.set(key, new Array(114514 * 19));
// 手動(dòng)清理一下垃圾 觀察內(nèi)存占用情況
global.gc();
console.log(
`第二次垃圾回收,當(dāng)前內(nèi)存使用情況:${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)}MB,
當(dāng)前Map的長(zhǎng)度: ${m.size}`
);
// 此時(shí)把 key鍵 的引用進(jìn)行斷開,并觀察內(nèi)存占用情況
key = null;
global.gc();
console.log(
`第三次垃圾回收,當(dāng)前內(nèi)存使用情況:${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)}MB,
當(dāng)前Map的長(zhǎng)度: ${m.size}`
);
// 清除Map所有鍵值對(duì)
m.clear();
global.gc();
console.log(
`第四次垃圾回收,當(dāng)前內(nèi)存使用情況:${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)}MB,
當(dāng)前Map的長(zhǎng)度: ${m.size}`
);
$ node --expose-gc index.js
第一次垃圾回收,當(dāng)前內(nèi)存使用情況:1.66MB
第二次垃圾回收,當(dāng)前內(nèi)存使用情況:18.45MB,當(dāng)前Map的長(zhǎng)度: 1
第三次垃圾回收,當(dāng)前內(nèi)存使用情況:18.45MB,當(dāng)前Map的長(zhǎng)度: 1
第四次垃圾回收,當(dāng)前內(nèi)存使用情況:1.85MB,當(dāng)前Map的長(zhǎng)度: 0
由此可見Map所構(gòu)建的實(shí)例是需要手動(dòng)清理,才能被垃圾回收清除,
而WeakMap只要外部的引用消失,所對(duì)應(yīng)的鍵值對(duì)就會(huì)自動(dòng)被垃圾回收清除。
總結(jié)
通過堆內(nèi)存分析后重新認(rèn)識(shí)Map和WeakMap,
由于一開始接觸這個(gè)API的時(shí)候有點(diǎn)陌生,
查閱很多網(wǎng)上很多文章后描述的樣子非常表層一筆帶過
因?yàn)槿跻梅凑蜁?huì)被自動(dòng)回收就是了不會(huì)占用內(nèi)存balabala,總之就是一兩句話帶過
這讓我很頭疼,下定決定翻了很多的書結(jié)合理解才搞懂WeakMap存在的意義。
前期是在在《現(xiàn)代JavaScript教程》受到啟發(fā),
通過翻查《深入淺出Node.js》找到了驗(yàn)證方法,
最后看到新版《JavaScript高級(jí)程序設(shè)計(jì)(第四版)》對(duì)WeakMap描述也是簡(jiǎn)單帶過,
于是下定決心來寫這一篇文章。
第一次產(chǎn)出文章,有表達(dá)描述不到位,或者代碼邏輯錯(cuò)誤的地方歡迎指出。
結(jié)束語
如果你對(duì)我的解釋有疑問,請(qǐng)給我留言,歡迎大家一起討論這些JS難題。
本文完~
喜歡本文的朋友,歡迎關(guān)注公眾號(hào)?前端陽光,收看更多精彩內(nèi)容
點(diǎn)個(gè)[在看],是對(duì)我最大的支持!
如果覺得這篇文章還不錯(cuò),來個(gè)【分享、點(diǎn)贊、在看】三連吧,讓更多的人也看到~


原文鏈接: https://www.zhihu.com/people/yiqun-a-qia