想要成爲一名 JavaScript 開發者,那麼你必須知道 JavaScript 程序內部的執行機制。執行上下文和執行棧、詞法做用域、this、內存空間、變量對象等都是JavaScript中關鍵點,同時也是JavaScript難點。前端
由於JavaScript具備自動垃圾回收機制,因此對於前端開發來講,內存空間並非一個常常被說起的概念,很容易被你們忽視。segmentfault
在很長一段時間裏認爲內存空間的概念在JS的學習中並非那麼重要。但是後我當我回過頭來從新整理JS基礎時,發現因爲對它們的模糊認知,致使了不少東西我都理解得並不明白。好比最基本的引用數據類型和引用傳遞究竟是怎麼回事兒?好比淺複製與深複製有什麼不一樣?還有閉包,原型等等。數組
JavaScript中並無嚴格意義上區分棧內存與堆內存。所以咱們能夠粗淺的理解爲JavaScript的全部數據都保存在堆內存中。可是在某些場景,咱們仍然須要基於堆棧數據結構的思路進行處理,好比JavaScript的執行上下文。瀏覽器
經過下圖類比如下咱們的棧: bash
堆存取數據的方式,則與書架與書很是類似,只要知道書的名字,咱們就能夠很方便的取出咱們想要的書,而不用像從乒乓球盒子裏取乒乓同樣,非得將上面的全部乒乓球拿出來才能取到中間的某一個乒乓球。數據結構
js的執行上下文建立以後會生成一個變量對象,咱們的基本數據類型基本都存儲在了這裏面。閉包
嚴格意義上來講,變量對象也是存放於堆內存中,可是因爲變量對象的特殊職能,咱們在理解時仍然須要將其於堆內存區分開來。函數
咱們的代碼執行是在一個一個的執行上下文中進行的。咱們聲明的一些變量都存放在了相應的變量對象裏~性能
基礎數據類型都是一些簡單的數據段,JavaScript中有5中基礎數據類型,分別是Undefined、Null、Boolean、Number、String。基礎數據類型都是按值訪問,由於咱們能夠直接操做保存在變量中的實際的值。學習
JS的引用數據類型,好比數組Array,它們值的大小是不固定的。引用數據類型的值是保存在堆內存中的對象。JavaScript不容許直接訪問堆內存中的位置,所以咱們不能直接操做對象的堆內存空間。在操做對象時,其實是在操做對象的引用而不是實際的對象。所以,引用類型的值都是按引用訪問的。這裏的引用,咱們能夠粗淺地理解爲保存在變量對象中的一個地址,該地址與堆內存的實際值相關聯。
例如:
var a1 = 0; // 變量對象
var a2 = 'this is string'; // 變量對象
var a3 = null; // 變量對象
var b = { m: 20 }; // 變量b存在於變量對象中,{m: 20} 做爲對象存在於堆內存中
var c = [1, 2, 3]; // 變量c存在於變量對象中,[1, 2, 3] 做爲對象存在於堆內存中
複製代碼
a1 a2 a3 b c 變量都被放入了變量對象,b c存儲的是實際引用對象的堆內存地址,訪問時屬於引用訪問。
// demo01.js
var a = 20;
var b = a;
b = 30;
// 這時a的值是多少?
複製代碼
// demo02.js
var m = { a: 10, b: 20 }
var n = m;
n.a = 15;
複製代碼
雖然js有本身的自動垃圾回收機制,咱們能夠不用太多的管理,js會在CPU空閒時刻按期去清理內存空間。可是咱們瞭解js的內存管理,可讓咱們更好的理解js的執行流程,幫組咱們寫出性能更好的代碼。
var a = 20; // 在內存中給數值變量分配空間
alert(a + 100); // 使用內存
a = null; // 使用完畢以後,釋放內存空間
複製代碼
上面就是一個簡單的內存釋放案例,js的垃圾回收機制其實就是去查找那些再也不被引用或者再也不被使用的內存空間,而後釋放掉。最經常使用的就是標記清除的方式,js會從根部也就是全局開始對各變量進行標記,層層標記下去,知道全部變臉都被標記,這些標記代表了變量的使用狀態,垃圾回收機制會根據這些標記去高效的清除那些再也不被使用的變量的地址空間,以及那些互相引用,可是不能被根訪問到的變量空間(函數內部)。
在局部做用域中,當函數執行完畢,局部變量也就沒有存在的必要了,所以垃圾收集器很容易作出判斷並回收。可是全局變量何時須要自動釋放內存空間則很難判斷,所以在咱們的開發中,須要儘可能避免使用全局變量,以確保性能問題。
在js中咱們確定須要知道咱們的變量和函數在哪裏聲明的,咱們怎麼找到並訪問?因此咱們還得靠執行上下文來幫忙,咱們知道代碼的執行是在一個一個的執行上下文中進行的,最外層的全局上下文、函數的函數執行上下文。
執行上下文的組成爲:變量對象、做用域鏈、this。 執行上下文的生命週期能夠分爲兩個階段:建立階段、執行階段。
變量對象(Variable Object)是一個與執行上下文相關的數據做用域,存儲了在上下文中定義的變量和函數聲明,先來看一段代碼示例:
全局而言,全局對象是window,全局上下文有一個特殊之處就是他的變量對象就是window。this指向也是window.咱們在全局聲明的變量和函數都會存儲在window中。
// 以瀏覽器中爲例,全局對象爲window
// 全局上下文建立階段
// VO 爲變量對象(Variable Object)的縮寫
windowEC = {
VO: Window,
scopeChain: {},
this: Window
}
複製代碼
變量對象存儲了執行上下文中的變量和函數聲明,但在函數上下文中,還多了一個arguments(函數參數列表), 一個僞數組對象。而且這裏的VO是經過arguments來初始化的。
一、建立arguments對象。檢查當前上下文中的參數,創建該對象下的屬性與屬性值。
二、檢查當前上下文的函數聲明,也就是使用function關鍵字聲明的函數。在變量對象中以函數名創建一個屬性,屬性值爲指向該函數所在內存地址的引用。若是變量對象已經存在相同名稱的屬性,則徹底替換這個屬性(函數是第一公民)。
三、檢查當前上下文中的變量聲明(var 聲明的變量),默認爲 undefined;若是變量名稱跟已經聲明的形式參數或函數相同,爲了防止同名的函數被修改成undefined,則會直接跳過變量聲明,原屬性值不會被修改。
上訴是VO變量對象的建立過程~~咱們平時說的變量的提高、函數的提高其實就是在說這裏,好比:
console.log(foo);
foo();//能夠執行,此時foo是函數
var foo=10; // foo被從新賦值爲10
foo();//foo已經被賦值爲一個變量,沒法執行foo爲函數,會報錯
console.log(foo); // 10
function foo(){
var a;
console.log(a);
a=12;
console.log(a);
}
console.log(foo); // 10
複製代碼
// 建立變量對象以下:
VO = {
arguments: {
length: 0
},
foo: function(),
}
複製代碼
在看一個例子:
alert(a);//輸出:function a(){ alert('我是函數') }
function a(){ alert('我是函數') }//
var a = '我是變量';
alert(a); //輸出:'我是變量'
複製代碼
有個細節必須注意:當遇到函數和變量同名且都會被提高的狀況,函數聲明優先級比較高,所以變量聲明會被函數聲明所覆蓋,可是能夠從新賦值。
固然還須要注意的是,函數未進入執行階段以前,變量對象中的屬性都不能訪問!可是進入執行階段以後,變量對象(VO)轉變爲了活動對象(AO),而後開始進行執行階段的操做。
執行上下文就是咱們當前代碼執行所處的環境的一個抽象概念,咱們的代碼執行是在一個一個的執行上下文中進行的。
爲了方便管理咱們的執行上下文,以及如何調配處理執行上下文的執行順序,咱們引入了執行上下文棧,用於方便咱們存放執行上下文和調用執行上下文。
先來一個直觀的例子:
var a = 1;
function foo() {
var b = 2;
function bar() {
console.log(b)
}
bar()
console.log(a);
}
foo()
複製代碼
1.執行這段代碼,首先會建立全局上下文globleEC,並推入執行上下文棧中;
2.當調用foo()時便會建立foo的上下文fooEC,並推入執行上下文棧中;
3.當調用bar()時便會建立bar的上下文barEC,並推入執行上下文棧中;
4.當bar函數執行完,barEC便會從執行上下文棧中彈出;
5.當foo函數執行完,fooEC便會從執行上下文棧中彈出;
6.在瀏覽器窗口關閉後,全局上下文globleEC便會從執行上下文棧中彈出;
執行上下文的生命週期包括三個階段:建立階段→執行階段→回收階段,重點是建立階段。
建立階段其實就是建立變量對象、建立做用域漣、綁定this指向。
JavaScript 引擎建立了執行上下文棧來管理執行上下文。能夠把執行上下文棧認爲是一個存儲函數調用的棧結構,遵循先進後出的原則。