首先,咱們要知道javascript是單線程、解釋性語言。所謂解釋性語言,就是翻譯一句執行一句。而不是通篇編譯成一個文件再去執行。javascript
其實這麼說尚未這麼直觀,讀一句執行一句那是到最後的事了。到JS執行前還有兩大步驟。java
那就是1.語法分析(或語意分析)→2.預編譯→3.解釋執行(真正的讀一句執行一句)閉包
第一步:語法分析(即掃描一下看一看有沒有低級的語法錯誤,好比多個大括號啊,寫個中文標點等等,只通篇檢查語法,但不執行。這就是語法分析的過程。)函數
第二步:預編譯過程(發生在函數執行時,也可說成執行的前一刻,下面重點講解)學習
第三步:解釋執行(解釋一句執行一句)spa
好了,瞭解了js執行的三大步驟接着說一下js預編譯。說預編譯以前先看幾段代碼線程
function test() { console.log(123456); } test();
上邊這段代碼毫無疑問能夠執行,正常輸出123456翻譯
接下來換一種寫法,先寫執行語句,再寫函數體,以下:code
test(); function test() { console.log(123456); }
這樣依然能夠正常執行。打印出123456對象
再看下邊這段代碼:
var num = 123; console.log(num);
這個也毫無疑問能夠執行,輸出123。
可是,若是直接這樣寫
console.log(num);
這樣屬於一個變量未經聲明就被訪問,會報錯。
再換一下寫法:
console.log(num); var num = 123;
其實這也是一種變量未經聲明就訪問,可是這樣寫不但不會不報錯,還能夠打印出結果,打印結果爲undefined。
這是爲何呢? 這時候,有些經驗的人會讓你記住兩句話:
1.函數聲明總體提高(意思是函數的聲明不管寫到那個位置,在執行的時候都會把函數聲明的語句提到最前執行)。
2.變量的聲明提高(意思是變量的聲明不管寫到什麼位置,在執行的時候都會提到最前執行,這裏注意是變量的聲明,沒有賦值什麼事)。
這兩句話雖然能夠解決大部分問題,可是下面的實例它就解決不了了,要真正解釋這兩種現象就不得不說預編譯了。學會了預編譯之後上邊那兩句話永遠不須要去記,輕鬆解決各類問題。
下邊來看一個實例:
function test(a) { console.log(a);
console.log(b);
console.log(c); var a = 123; console.log(a); function a() {}; console.log(a); var b = function () {}; console.log(b); function c() {}; console.log(c); } test(1);
這段代碼就是上邊那兩句話解決不了的。先思考一下這段代碼的運行結果會是什麼呢?
想要明白這個運行結果,首先咱們得明白一個事,這裏邊既有函數,又有變量聲明,還有形參,並且你們的名字還都同樣,像打仗同樣,都在搶的用。到底誰能搶過誰呢?
咱們已經知道函數的預編譯在函數執行的前一刻了,也就是說在函數運行以前函數的預編譯就幫助咱們調和了這個「打仗」的矛盾。
函數的預編譯分爲4大步驟。
第一步:生成一個Activation Object(執行期上下文)對象,簡稱AO對象。在訪問函數中的變量的時候會直接從咱們函數對應的的AO中獲取。
AO{
}
這就是一個AO對象。
第二步:找形參和變量聲明,將形參和變量名做爲AO對象的屬性名,值爲undefined。
注意:var a=123;這條語句須要拆分紅兩部分,一部分爲var a;(變量的聲明) 一部分爲a=123;(變量的賦值)。在這裏咱們找的是變量聲明。因此a=123並無在預編譯 過程當中發現。
因此對於上邊的函數:
AO{
a:undefined,
b:undefined
}
第三步:將實參值和形參統一
此時
AO{ a:1, b:undefined }
第四步:在函數體裏找函數聲明,值賦予函數體
注意:這裏找的是函數聲明,而b=function () {};屬於函數表達式,不是這裏須要的。
因此此時
AO{ a:function a(){}, b:undefined, c :function c(){} }
以上AO就是函數的預編譯所有完成以後的AO。
接下來該到了真正的讀一句執行一句的時候了。
1.讀console.log(a);語句,從AO中找到a的值: function a(){},因此輸出結果就爲 function a(){} 。
2.讀console.log(b);語句,從AO中找到b的值: undefined,因此輸出結果就爲 undefined。
3.讀console.log(c);語句,從AO中找到c的值: function c(){},因此輸出結果就爲 function c(){} 。
4.讀var a = 123;語句,var a = 123分爲var a;和a=123;兩部分,第一部分變量的聲明看過,如今只看a=123;此時:
AO{ a:123, b:undefined, c :function c(){} }
5.讀console.log(a);語句,從AO中找到a的值: 123,因此輸出結果就爲 123 。
6.function a(){};語句在預編譯時已經看過,如今無論,直接下一句console.log(a); 從AO中找到a的值: 123,因此輸出結果就爲 123 。
7.var b = function() {};語句一樣也是分爲var b;和 b = function() {};兩部分,第一部分變量的聲明看過,如今只看 b = function() {};此時:
AO{ a:123, b: function () {}, c :function c(){} }
8.讀console.log(b);語句,從AO中找到b的值: function() {},因此輸出結果就爲 function() {}。
9.function c(){};也在預編譯中看過了,在這裏不看,直接下一句console.log(c);從AO中找到c的值: function c() {},因此輸出結果就爲 function c() {}。
運行結果以下圖所示:
以上四部曲說的是Javascript函數的預編譯,預編譯不只發生在函數體,在全局也會發生預編譯。全局的預編譯相對於函數的就簡單一些了。接着咱們看一下全局的預編譯。
全局的預編譯只有三個步驟,由於在全局不會涉及到參數。
繼續來看一個發生在全局的預編譯的實例
console.log(a); console.log(b); var a = 123; var b = function (){}; console.log(a); function a() {}; console.log(a); console.log(b);
思考一下這段代碼的運行結果。一樣也是有變量聲明,函數名,只不過發生在全局不會有參數的出現,其實步驟與函數的預編譯一致,只是去掉有關參數的部分便可。
第一步:生成一個Global Object(執行期上下文)對象,簡稱GO對象。由於是全局生成的再也不叫AO,可是道理和AO同樣,能夠理解爲換一種叫法而已。
GO{
}
這就是一個GO對象
第二步:在全局中找變量聲明,將變量名做爲GO對象的屬性名,值爲undefined。
一樣須要注意:var a=123;這條語句須要拆分紅兩部分,一部分爲var a;(變量的聲明) 一部分爲a=123;(變量的賦值)。在這裏咱們找的是變量聲明。因此a=123並無在預編譯過程當中發現。
此時GO是這樣的:
GO{
a:undefined,
b:undefined
}
因爲沒有參數,實參形參統一的步驟直接省略
第三步:在全局中找函數聲明,值賦予函數體
一樣須要注意:這裏找的是函數聲明,而b=function () {};屬於函數表達式,不是這裏須要的。
因此此時:
GO{
a: function a() {},
b: undefined
}
接下來該到了真正的讀一句執行一句的時候了。
1.讀console.log(a);語句,在GO中找到a的值:function a(){},因此輸出結果就爲function a(){}。
2.讀console.log(b);語句,在GO中找到b的值:undefined,因此輸出結果就爲undefined。
3.讀var a = 123;語句,var a = 123分爲var a;和a=123;兩部分,第一部分變量的聲明看過,如今只看a=123;此時:
GO{
a:123,
b:undefined
}
4.讀var b = function() {};語句一樣也是分爲var b;和 b = function() {};兩部分,第一部分變量的聲明看過,如今只看 b = function() {};此時:
GO{ a:123, b:function() {} }
5.讀console.log(a);語句,在GO中找到a的值:123,因此輸出結果就爲123。
6.function a() {};語句在預編譯時已經看過,如今無論,直接下一句console.log(a); 從GO中找到a的值: 123,因此輸出結果就爲 123 。
7.讀console.log(b);語句,在GO中找到b的值:function (){},因此輸出結果就爲function (){}。
運行結果以下圖所示:
這裏有一個特別的:未經聲明的變量就直接賦值,該變量歸GO全部。什麼意思呢?咱們看下邊的實例
function f() { var a = b = 6; c = 8; } f(); console.log(a);
這段代碼會報錯,由於變量a在函數中聲明,他歸該函數的AO全部,當函數執行完AO被銷燬,因此在全局找不到a。
可是這樣:
function f() { var a = b = 6; c = 8; } f(); console.log(b); console.log(c);
運行結果:
在全局訪問b和c不但沒報錯,並且還正確的打印出了運行結果。正如咱們剛剛所說的未經聲明的變量就直接賦值,該變量歸GO全部。因此在全局能夠訪問到也是順其天然的事情了。
好了,以上就是javascript的預編譯過程。說了半天,學習這個預編譯到底有什麼用呢?在開發的時候咱們也不可能這麼命名變量與函數名的呀。其實在這裏學習預編譯主要是爲了下面的做用域來作鋪墊,理解了做用域以後再談咱們開發中常見的閉包。這樣才能更深刻的去理解閉包。