一步一步的理解javascript的預編譯

首先,咱們要知道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的預編譯過程。說了半天,學習這個預編譯到底有什麼用呢?在開發的時候咱們也不可能這麼命名變量與函數名的呀。其實在這裏學習預編譯主要是爲了下面的做用域來作鋪墊,理解了做用域以後再談咱們開發中常見的閉包。這樣才能更深刻的去理解閉包。

相關文章
相關標籤/搜索