內存由可讀寫單元組成,表示一片連續可操做的空間。在編程時,能夠經過主動操做來申請,使用和釋放可操做空間。內存管理指的就是主動操做過程,也就是申請內存,使用內存和釋放內存。算法
// 申請內存 let str // 使用內存 str = 'foo' // 釋放內存 str = null // 再也不引用,垃圾回收會自動回收內存
當內存再也不被使用時,其會被視爲垃圾,而後被釋放(回收)。編程
在JavaScript中,垃圾回收是自動進行的。
如何判斷垃圾內存?數組
「根」在js中,能夠將根看做全局對象。不能從根上訪問到指的就是不能從全局對象上經過某條路徑找到,能夠是直接掛載在全局對象上,也能夠是間接掛載在全局對象上。
function fn(obj1, obj2) { obj1['next'] = obj2 obj2['pre'] = obj1 return { o1: obj1, o2: obj2 } } const obj = fn()
上述代碼的關係以下圖所示,此時obj,obj1,obj2均可以從全局對象上找到,所以不能看成垃圾被回收。
以下圖所示,若是經過delete將obj的o1屬性和obj2的prev屬性刪除,那麼obj1就沒法從全局對象上找到,此時obj1將會被看成垃圾回收。瀏覽器
可到對象指的是能訪問到的對象,訪問的方式能夠是引用,也能夠是經過做用域鏈查找到。
判斷一個對象是不是可達對象的標準就是從根出發是否能夠被找到。緩存
GC能夠理解爲是垃圾回收機制的簡寫。算法也就指的是查找垃圾,回收垃圾的規則。
經常使用的GC算法包含如下幾個:微信
經過引用計數器設置內存的引用數,當內存的引用關係發生改變的時候修改引用數,當引用數爲0的時候內存當即被回收。閉包
// {name: 'zs'}所在的空間是一塊內存 // 此時obj1引用這塊內存,因此引用計數器上記爲1 let obj1= {name: 'zs'} // obj2 一樣引用了這塊內存,因此引用計數器爲2 let obj2 = obj1 // obj1 再也不引用這塊內存,因此計數器變爲1 obj1 = null // obj2也再也不引用這塊內存,此時計數器爲0.這塊內存會被看成垃圾回收 obj2 = null
算法優勢:app
算法缺點:dom
function fn() { const obj1 = { name: 'zs' } const obj2 = { name: 'ls' } // 在方法執行完畢之後,obj1和obj2應該被看成垃圾被回收,可是因爲其相互引用,此時引用計數器上不爲0, 因此沒法回收 obj1['friend'] = obj2 obj2['friend'] = obj1 } fn()
標記清除算法將垃圾回收分爲標記和刪除階段,其算法步驟以下:jsp
以下圖所示,第一不找到全部活動對象,因爲ABCDE能夠經過全局對象找到,因此被標記,a1和b1不能經過全局對象找到,因此不會被標記。第二步,找到沒有被標記的a1和b1,將其看成垃圾回收。
與引用計數算法相比。
優勢:
缺點:
假設內存中有一段連續的內存空間ABCDEF,若是BCDE被標記爲活動對象,AB和F沒有被標記,那麼AB,F會被看成垃圾回收。回收完成後,形成存在AB和F兩個碎片內存能夠被使用,其只能放入對應長度的數據。
標記整理算法和標記清除算法相似,只是多了整理內存步驟。
經過整理,能夠解決標記清除算法形成內存碎片化的問題。
V8是一款主流的JavaScript執行引擎,採用即時編譯,內存有限制(64位1.5G,32位800M)。
js中的數據分爲原始數據和對象引用數據兩種,其中原始數據是由語言自己去處理,因此此處的垃圾回收策略主要針對棧上的對象引用數據。
V8採起分代回收的策略,因爲v8對內存大小有限制,因此其將內存分紅新生代和老生代兩種,不一樣的生代採起不一樣的垃圾回收策略。
V8主要採起的GC算法有以下:
V8將內存分爲兩塊,其中小的空間稱爲新生代(64位32M/32位16M),其主要存儲存活時間較短的對象。新生代內部一樣分爲兩個等大小的空間From和To,經過空間複製和標記整理兩個算法完成垃圾回收。
From到To的拷貝過程可能出發晉升,也就是重新生代拷貝到老生代,下面兩種狀況將出發晉升。
老生代指的是空間較大的內存塊(64位1.4G,32位700M),其內部存儲存活時間長的對象,採用標記清除,標記整理和增量標記三種算法實現垃圾回收。
js代碼在瀏覽器中執行的時候,可能出現的和內存相關的問題以下:
全局變量會致使的問題以下:
將不可避免的全局變量緩存到局部做用域中,減小查找時間,優化性能。適用於在局部做用域中頻繁使用某個全局變量。
function query() { // 在局部做用域中直接使用全局的document變量,在執行時,局部做用域找不到該變量,會沿着做用域鏈向上查找直到在全局中找到 return document.getElementsByTagName('input') } function query1() { // 經過將全局變量賦值給局部變量,那麼查找時直接在局部做用域找到,不用再向上查找 let dom = document return dom.getElementsByTagName('input') }
在爲全部的實例對象添加共享方法的時候,經過原型定義比在構造函數中經過this定義性能更好。這是因爲構造函數中this定義的方法在每一個實例中都會保存一份單獨的引用,而經過原型定義,全部的實例會指向同一個引用。
function Person() { // 每一個實例對象都會保存一份say的引用,10個就會有10個內存引用 this.say = function () { console.log(1) } } const zs = new Person() function Person1() { } // 全部實例的原型都指向一個內存引用,減小內存開銷 Person1.prototype.say = function () { console.log(1) } const ls = new Person1()
閉包是指在外部做用域中可使用內部做用域中的變量。
function foo() { let str = 'foo' return function () { console.log(str) } } let f = foo() // f在外部執行的時候依然可以訪問foo做用域中的str變量 f()
閉包是一種常見寫法,能夠解決js編程中的不少問題,可是因爲內部做用域中的變量被外部引用,因此此變量不能被垃圾回收,若是使用不當很容易形成內存泄露,所以在編程中不能爲了閉包而閉包。
js在編寫類的時候,很容易的出如今類上提供一個方法,該方法用於訪問類內部的一個屬性。
function Person() { this.name = 'foo' // 爲了便於控制,在屬性的訪問上添加了一層 this.getName = function () { return this.name } } const zs = new Person() console.log(zs.getName) function Person1() { this.name = 'foo' } const ls = new Person1() // 直接訪問屬性 console.log(ls.name)
經過jsperf測試,發現直接訪問會比包裝訪問要快的多。所以拋開代碼編寫規範,單從執行速度上來說,直接訪問更快。
let arr = Array(100).fill('foo') // 每次循環都要獲取數組長度 for (let i = 0; i < arr.length; i++) { console.log(i) } // 緩存數組長度, for (let i = 0, len = arr.length; i < len; i++) { console.log(i) }
緩存數組長度for循環執行速度要更快,特別適合很是大或者很是複雜的數組遍歷。
let arr = Array(100).fill('foo') arr.forEach(function (item) { console.log(item) }) for (let i = 0, len = arr.length; i < len; i++) { console.log(i) } for (let i in arr) { console.log(arr[i]) }
經過jsperf工具發現,forEach的執行速度最快,所以在不影響功能的前提下,儘可能使用forEach可加快代碼的執行速度。
在日常的js代碼編寫過程當中,經常伴有Dom節點的添加,因爲Dom節點添加操做經常伴有迴流和重繪,這兩個操做比較耗時,可使用文檔碎片優化這種耗時操做。
for (let i = 0; i < 10; i++) { let p = document.createElement('p') document.body.append(p) } let fraEls = document.createDocumentFragment() for (let i = 0; i < 10; i++) { let p = document.createElement('p') fraEls.append(p) } document.body.append(fraEls)