淺談js的內存與閉包

本文來自於個人github

0.前言

主要結合了內存的概念講了js的一些的很簡單、可是又不當心就犯錯的地方。 結論:js執行順序,先定義,後執行,從上到下,就近原則。閉包可讓外部訪問某函數內部變量,並且會致使內存泄漏。javascript

1.先說類型

在ECMAscript數據類型有基本類型和引用類型,基本類型有Undefined、Null、Boolean、Number、String,引用類型有Object,全部的的值將會是6種的其中之一(數據類型具備動態性,沒有定義其餘數據類型的必要了) 引用類型的值,也就是對象,一個對象是某個引用類型的一個實例,用new操做符建立也能夠用字面量的方式(對象字面量建立var obj ={ })。ECMA裏面有不少原生的引用類型,就是查文檔的時候看見的那些:Function、Number (是對於原始類型Number的引用類型)、String(是對於原始類型String的引用類型)、Date、Array、Boolean(...)、Math、RegExp等等。 在程序運行的時候,整塊內存能夠劃分爲常量池(存放基本類型的值)、棧(存放變量)、很大的堆(存放對象)、運行時環境(函數運行時)前端

1

基本數據類型的值是直接在常量池裏面能夠拿到,而引用類型是拿到的是對象的引用vue

var a = 1;
var b = 'hello';
var c = a;
複製代碼

c = a,這種基本數據類型的複製,只是從新複製一份獨立的副本,在變量的對象上建立一個新的值,再把值複製到新變量分配的位置上,a、c他們本身的操做不會影響到對方。java

a++;console.log(a);console.log(c)
複製代碼

顯然是輸出二、1node

obj1和obj2,拿到的是新建立的對象的引用(也就是家裏的鑰匙,每一個人帶一把),當操做對象的時候,對象發生改變,另外一個obj訪問的時候,發現對象也會改。就像,家裏有一我的回去搞衛生了,另外一個回家發現家裏很乾淨了。git

var obj1 = new Object();
obj1.name = 'obj1'
var obj2 = obj1
console.log(obj2)  //{name: "obj1"}
複製代碼

2

對於vue,爲何data必須是一個返回一個對象的函數,也是這個道理,避免全部的vue實例共用一套data。因此對於相似於這種狀況,咱們能夠像vue那樣處理github

//data是一個對象的時候,共用一套data
function D(){}
D.prototype.data =  {a:1,b:2}
var a = new D()
var b = new D()
a.data.a = 666
b.data.a //666
//data是一個函數的時候,各自維護本身的data
function D(){
	this.data = this.data()
}
D.prototype.data = function () {
	return {
		a:1,b:2
	}
}
var a = new D()
var b = new D()
a.data.a = 666
b.data.a //1
複製代碼

一樣的身爲引用類型的函數也是同理算法

var a = function(){console.log(1)}
var b = a;
a = null;
b();a()
//b輸出1,a報錯:Uncaught TypeError: a is not a function
//a指向函數,b拿到和a同樣的指針,而後讓a指向空
複製代碼

把a變成null,只是切斷了a和函數之間的引用關係,對b沒有影響後端

2.再說順序

你們常據說的先定義後執行,其實就是在棧中先開闢一塊內存空間,而後在拿到他所對應的值,基本類型去常量池,引用類型去堆拿到他的引用。你們常說的原始類型值在棧,其實就是這種效果。 數組

3

2.1 爲何引用類型值要放在堆中,而原始類型值要放在棧

在計算機的數據結構中,棧比堆的運算速度快,Object是一個複雜的結構且能夠擴展:數組可擴充,對象可添加屬性,均可以增刪改查。將他們放在堆中是爲了避免影響棧的效率。而是經過引用的方式查找到堆中的實際對象再進行操做。 所以又引出另外一個話題,查找值的時候先去棧查找再去堆查找。

2.2 爲何先去棧查找再去堆查找

既然都講了,棧比堆的運算速度,堆存放的是複雜數據類型。那麼簡單來講,寧願大海撈針呢仍是碗裏撈針呢?

3.而後到了函數

先拋出一個問題

function a(){console.log(2)};
var a  = function(){console.log(1)};
a()
複製代碼

覆蓋?那麼交換的結果又是什麼呢?

var a  = function(){console.log(1)};
function a(){console.log(2)};
a()
複製代碼

都是1,而後有的人就說了,var優先。好的,那爲何var優先?

4

先定義後執行,先去棧查找

變量提高,其實也是如此。先定義(開闢一塊內存空間,此時值能夠說是undefined)後執行(從上到下,該賦值的就賦值,該執行操做的就去操做),就近原則 函數聲明和函數表達式,有時候不注意,就不當心出錯了

a(); function a(){console.log(666)}//666
複製代碼

另外一種狀況:

a(); var a = function (){console.log(666)}//a is not a function
複製代碼

雖然第一種方法有變量提高,不會出錯,正常來講,仍是按順序寫,定義語句放前面。若是想嚴格要求本身,就手動來個嚴格模式‘use strict’吧。對於框架的開發,須要嚴謹遵照規則,因此通常會用嚴格模式。

4.接着是臨時空間

函數執行的時候,會臨時開闢一塊內存空間,這塊內存空間長得和外面這個同樣,也有本身的棧堆,當函數運行完就銷燬。

4.1 eg1:

var a = 10;
function() {
console.log(a);//undefined
var a = 1;
console.log(a)//1
}
複製代碼

宏觀來講,只有2步一和二,當執行第二步,就跳到函數內部執行②-⑧

5
函數外部的a=10徹底就沒有關係,這裏面形成undefined主要由於變量提高,其實準確的順序是:

var a
console.log(a);//undefined
a = 1;
console.log(a)//1
複製代碼

爲何不出去找全局的a? 就近原則。爲何就近原則?都肯定函數內部有定義了,就不會再去外面白費力氣。實際上是,函數在本身的做用域內找到就不會再再繼續找,相似原型鏈同樣,在構造函數裏面找到某個屬性就不會去原型找,找不到纔去,再找不到就再往上。函數也是,沿着做用域鏈查找。相似的一個例子,咱們用函數聲明定義一個函數f,再用一個變量g拿到這個函數的引用,而後在外面用f是訪問不了這個函數的,可是在函數內部是能找到f這個名字的:

var g = function f(){
   console.log(f)
    }
g()//打印整個函數
f()//報錯
複製代碼

4.2 eg2

function f(){
return function f1(){
       console.log(1)
   }
};
var res = f();
res();
f1()
複製代碼

res(),返回的是裏面的函數,若是直接f1()就報錯,由於這是window.f1()

6

  • 函數聲明後,能夠經過引用名稱查找或者內存地址查找
  • 局部做用域用function聲明,聲明不等於建立,只有調用函數的時候才建立
  • 函數f有內存地址的話,經過棧找f的內存空間,若是找不到棧中f這個變量,就去堆中找

5.垃圾回收

進行前端開發時幾乎不須要關心內存問題,V8限制的內存幾乎不會出現用完的狀況,並且咱們只要關閉了瀏覽器,一切都結束。若是是node後端,後端程序每每進行更加複雜的操做,加上長期運行在服務器不重啓,若是不關注內存管理,聚沙成塔就會致使內存泄漏。 node中的內存第一個部分仍是和上面的同樣,有棧、堆、運行時環境,另外還有一個緩衝區存放Buffer。你能夠經過process.memoryUsage()查看node裏面進程內存使用狀況。堆中的對象,被劃分爲新生代和老生代,他們會被不一樣的垃圾回收機制清理掉。

5.1新生代

新生代用Scavenge算法進行垃圾回收,利用複製的方式實現內存回收的算法。 他的過程是:

  • 將新生代的總空間一分爲二,只使用其中一個,另外一個處於閒置,等待垃圾回收時使用。使用中的那塊空間稱爲From,閒置的空間稱爲To
  • 當觸發垃圾回收時,V8將From空間中全部存活下來的對象複製到To空間。
  • From空間全部應該存活的對象都複製完成後,本來的From空間將被釋放,成爲閒置空間,本來To空間則成爲使用中空間,也就是功能交換。
  • 若是某對象已經經歷一次新生代垃圾回收並且第二次依舊存活,或者To空間已經使用了25%,都會晉升至老生代

1

5.2老生代

老生代利用了標記-清除(後面又加上了標記-整理)的方式進行垃圾回收。 在標記階段(週期比較大)遍歷堆中的全部對象,標記活着的對象,在隨後的清除階段中,只清除沒有被標記的對象。每一個內存頁有一個用來標記對象的位圖。這個位圖另外有兩位用來標記對象的狀態,這個狀態一共有三種:未被垃圾回收器發現、被垃圾回收器發現但鄰接對象還沒有所有處理、不被垃圾回收器發現但鄰接對象所有被處理。分別對應着三種顏色:白、灰、黑。

遍歷的時候,主要是利用DFS。剛剛開始的時候,全部的對象都是白色。從根對象開始遍歷,遍歷過的對象會變成灰色,放入一個額外開闢的雙端隊列中。標記階段的每次循環,垃圾回收器都會從雙端隊列中取出一個對象染成黑對象,並將鄰接的對象染色爲灰,而後把其鄰接對象放入雙端隊列。一直循環,最後全部的對象只有黑和白,白色的將會被清理。 假設全局根對象是root,那麼活對象必然是被鏈接在對象樹上面的,若是是死對象,好比var a = {};a=null咱們建立了一個對象,但把他從對象樹上面切斷聯繫。這樣子,DFS必然找不到他,他永遠是白色。 此外,在過程當中把垃圾對象刪除後,內存空間是一塊一塊地零星散亂地分佈,若是是遇到一個須要很大內存空間的對象,須要連續一大片內存存儲的對象,那就有問題了。因此還有一個整理的階段,把對象整理到在內存上連續分佈。

5.3 對比

  • 新生代是常常發生的,老生代發生的週期長
  • 新生代佔用的內存小,老生代佔用了大部份內存
  • 新生代須要把內存分紅兩塊進行操做,老生代不須要
  • 新生代是基於對象複製,若是對象太多,複製消耗也會很大,因此須要和老生代相互合做。老生代基於DFS,深度遍歷每個活對象
  • 顯然老生代花銷大,因此他的週期也長,可是比較完全

6.IIFE和閉包

6.1 IIFE

當即執行函數,造成一個沙盒環境,防止變量污染內部,是作各類框架的好方法 先手寫一段假的jQuery

(function(root){
 var $ = function(){
//代碼
}
root.$ = $
})(this)
複製代碼

這樣子在內部函數裏面寫相關的表達式,咱們就能夠用美圓符號使用jQuery(實際上jQuery第一個括號是全局環境判斷,真正的函數體放在第二個括號裏面,號稱世界上最強的選擇器sizzle也裏面)

7

6.2閉包

閉包的概念各有各的說法,平時人家問閉包是什麼,大概多數人都是說在函數中返回函數、函數外面能訪問到裏面的變量,這些顯而易見的現象,或者把一些長篇大論搬出來。簡單來講,就是外部訪問內部變量,並且內部臨時開闢的內存空間不會被垃圾回收。查找值的時候沿着做用域鏈查找,找到則中止。 對於js各類庫,是一個龐大的IIFE包裹着,若是他被垃圾回收了,咱們確定不能利用了。而咱們實際上就是能利用他,就是由於他暴露了接口,使得全局環境保持對IIFE內部的函數和變量的引用,咱們才得以利用。 各類書對於閉包的解釋: 《權威指南》:函數對象經過做用域鏈相互關聯起來,函數內部變量均可以保持在函數的做用域中,有權訪問另外一個函數做用域中的變量 《忍者祕籍》:一個函數建立時容許自身訪問並操做該自身函數之外的變量所建立的做用域 《你不知道的js》:是基於詞法的做用域書寫代碼時所產生的結果,當函數記住並訪問所在的詞法做用域,閉包就產生了 閉包的產生,會致使內存泄漏。 前面已經說到,js具備垃圾回收機制,若是發現變量被不使用將會被回收,而閉包相互引用,讓他不會被回收,一直佔據着一塊內存,長期持有一塊內存的引用,因此致使內存泄漏。

var b = 10
function a(){
	var b = 1
	return function c(){//暴露內部函數的接口
		console.log(b)
	}
}
a()()//1,外部拿到內部的引用,臨時開闢的內存空間不會被回收

//改寫成IIFE形式
var b = 10
var a = (function(){
	var b = 1
	return function c(){
		console.log(b)
	}
})()
a()//1

//改爲window對象的一個引用
var b = 10
(function(){
	var b = 1
	window.c =  function(){
		console.log(b)
	}
})()
c()//1

//多個閉包
function a(){
	var s = 1
	return function count(){
		s++
		console.log(s)
	}
}
var b = a()//至關於賦值
var c = a()
b()//2
b()//3
c()//2,各自保持各自的」賦值結果」,互相不干擾

//r被垃圾回收
function a(){
        var r = 1
	var s = 1
	return function count(){
		s++
		console.log(s)
	}
}
var b = a()//咱們能夠打個斷點,在谷歌瀏覽器看他的調用棧,發現閉包裏面沒有r了
複製代碼

對於最後一個例子,r、s並非像一些人認爲的那樣,有閉包了,r、s都會留下,實際上是r已經被回收了。在執行的函數時候,將會爲這個函數建立一個上下文ctx,最開始這個ctx是空的,從上到下執行到函數a的閉包聲明b時,因爲b函數依賴變量s ,所以會將 s 加入b的ctx——ctx2。a內部全部的閉包,都會持有這個ctx2。(因此說,閉包之因此閉包,就是由於持有這個ctx) 每個閉包都會引用其外部函數的ctx(這裏是b的ctx2),讀取變量s的時候,被閉包捕捉,加入ctx中的變量,接着被分配到堆。而真正的局部變量是r ,保存在棧,當b執行完畢後出棧而且被垃圾回收。而a的ctx被閉包引用,若是有任何一個閉包存活,他對應的ctx都將存活,變量也不會被銷燬。

image

咱們也據說一句話,儘可能避免全局變量。其實也是這樣的道理,一個函數返回另外一個函數,也就是分別把兩個函數按順序壓入調用棧。咱們知道棧是先進後出,那全局的變量(也處於棧底),越是不能獲得垃圾回收,存活的時間越長。但也許全局變量在某個時候開始就沒有做用了,就不能被回收,形成了內存泄漏。因此又引出另外一個常見的注意事項:不要過分利用閉包。用得越多,棧越深,變量越不能被回收。

瀏覽器的全局對象爲window,關閉瀏覽器天然一切結束。Node中全局對象爲global,若是global中有屬性已經沒有用處了,必定要設置爲null,由於只有等到程序中止運行,纔會銷燬。而咱們的服務器固然是長期不關機的,內存泄漏聚沙成塔,爆內存是遲早的事情。

Node中,當一個模塊被引入,這個模塊就會被緩存在內存中,提升下次被引用的速度(緩存代理)。通常狀況下,整個Node程序中對同一個模塊的引用,都是同一個實例(instance),這個實例一直存活在內存中。因此,若是任意模塊中有變量已經再也不須要,最好手動設置爲null,否則會白白佔用內存

相關文章
相關標籤/搜索