JS進階系列-JS執行期上下文(一)

二郎神楊戩發表於2020-10-02

點贊再看,年薪百萬 本文已收錄至https://github.com/likekk/-Blog歡迎大家star???,共同進步。如果文章有出現錯誤的地方,歡迎大家指出。後期將在將GitHub上規劃前端學習的路線和資源分享。

前言

距離寫上一篇文章已經過去兩個月了(上一篇文章是2020年07月17日寫的),託更有點嚴重,一方面是這幾個月專案在趕,另一方面是自己近兩個月以來變懶了(為自己不想更新文章找藉口)???,國慶假期,公司放八天假。

自己沒有去回家,主要是回家有點遠,從來這邊讀書到現在的國慶期間一直都沒有回過家,這七八天本來也打算出去玩的(肯定需要安排時間出去玩),但是想想還是學點東西比較好。於是有了這篇部落格。

寫預編譯(執行期上下文)這篇部落格的初衷是因為一方面方便自己複習,另一方面是為了後續出閉包、作用域和作用域鏈、this指向等等的部落格做一些前期的準備,以上幾個知識點可以說是在基礎中比較難以掌握的,也有可能會在一些面試中經常會問到的。

正文

首先在講解預編譯之前,我們先說下JavaScript的語言特點吧!說兩點比較重要的(比較優秀的兩點)

我們知道JavaScript語言首先是單執行緒的,其次是解釋性語言。對於這兩點我相信大家都不陌生,接著我們擴充開來,對於解釋執行只是發生在執行的最後一步,解釋執行之前還有兩步,簡單介紹一下JavaScript執行三部曲

  • 語法分析
  • 預編譯(全域性預編譯階段【建立GO物件】和函式預編譯階段【AO】)
  • 解釋執行

語法分析:

對於語法分析的話,大概過一下就可以了,比如:是否少寫個括號,是否有中文等等一系列的問題。JavaScript直譯器會全篇掃描一下,檢查是否有錯誤,但是不會執行。

預編譯:

這個就比較有意思了,也是本篇部落格的重點,預編譯的話主要分兩種吧!一種是函式體裡面的預編譯(AO),另一種是全域性環境的預編譯(GO),不懂?沒有關係,我會慢慢講到。

解釋執行:

對於解釋執行執行的話,我想我也可以不用多說吧!就是在執行的時候解釋一行執行一行唄!

對於預編譯這個概念其實我們有遇到過,只是我們不知道它的專業名詞叫做預編譯

納尼?不信我,好吧!我們看下是否遇到過,先簡單舉個例子來證明一下我的觀點

test();
function test(){
    console.log('a');
}
console.log(a);
var a=123;

提問:輸出什麼?

答案: a 和 undefined

相信大家都可以做出來,其實這裡面就已經包含預編譯的階段,最開始講的時候就說了,預編譯發生在函式執行的前一刻,所以在函式呼叫的時候就已經有預編譯這一個階段了。

好的,我們再來看下另外一個示例

test();
function test(){
 console.log('a');
}
test();
console.log(a);
var a=123;

答案: a 、 a 和 undefined

對於如此簡單的兩道題目,相信在座的各位沒有做不出來的吧!

路人甲:“楊戩哥,這兩道題目好簡單(Low)呀!”

楊戩:”是,是很簡單,但是你們知道是什麼原理嗎?為什麼第一題中輸出a和undefined,對於a的話相信大家都知道,但是undefined輸出是為什麼?大家是否有考慮過。“

路人乙:”楊戩哥,我們老師講過兩句比較有用的話“

函式宣告,整體提升

變數 宣告提升

路人丙:”這兩句話可以解決很多問題,我在碰到一些問題的時候,就是套用這兩句話。“

楊戩:”路人丙弟弟,你的這兩句話確實可以解決很多問題,但是有些問題靠這兩句話是解決不了的“

路人丙:”楊戩哥,我不信“

楊戩:”好吧!,既然你不信,那我就出道題考考你。“

路人丙:”come on“

function foo(a){
 console.log(a);
 var a=123;
 console.log(a);
 function a(){}
 console.log(a);
 var b=function(){}
 console.log(b);
 function d(){}
}
foo(1);

楊戩:”提問console.log()都輸出是什麼?“

路人丙:”這、這、這,還有這操作“

路人丙:”算了,我還是老老實實聽楊戩講吧!,不裝逼了“

我先公佈以下答案吧!

答案:function a(){}、123、123、function(){}

但是到這裡我還是沒有那麼快講解預編譯,考慮了一下,講解之前還是需要鋪墊一點東西,否則很難講清楚。

預編譯前期

預編譯前期主要講解兩個東西

1、imply global 暗示全域性變數,即任何變數,如果變數未經宣告就賦值,此變數就為全域性物件所有

2、一切宣告的全域性變數都是window屬性

例一

var a=123;
console.log(a);
function test(){
    var a=b=123;
}
test();
console.log(window.a);
console.log(window.b);

依次輸出undefined、123

看第一條,即任何變數,如果變數未經宣告就賦值,此變數就為全域性物件所有

a變數我們是已經宣告瞭,但是b變數我們並沒有宣告,此時就進行賦值,所以歸全域性物件所有

例二

  var b=123;
 console.log(b);
    console.log(window.b);

此時b===window.b

第二條,一切宣告的全域性變數都是window屬性

講解的不是那麼透徹,等講完全域性預編譯的時候再回過頭來看,你就會有一種醍醐灌頂的感覺

預編譯(執行期上下文)

預編譯在這裡我主要分兩種,一種是函式預編譯(函式執行期上下文)和全域性預編譯(全域性執行期上下文)

函式預編譯(函式執行期上下文)

函式預編譯我給大家總結了四條規律,無論什麼樣的,都能夠正確的避坑

  • 建立AO物件
  • 找形參和變數宣告,將變數的形參名作為AO的屬性名,值為undefined
  • 將實參值和形參進行統一
  • 在函式體裡面找函式宣告,值賦予函式體

根據這四條法則我們回到最開始的題目進行講解

例一

    function foo(a){
        console.log(a);
        var a=123;
        console.log(a);
        function a(){}
        console.log(a);
        var b=function(){}
        console.log(b);
        function d(){}
    }
    foo(1);

    /***
     * 1.建立AO物件
     * AO{
     *
     * }
     * 2.找形參和變數宣告,將變數的形參名作為AO物件的屬性名,值為undefined
     * AO{
     *     a:undefined
     *     b:undefined
     * }
     * 注:由於形參a和變數宣告a相同,取其中一個即可
     *
     * 3.將實參值和形參進行統一
     * AO{
     *     a:undefined=>a:1,
     *     b:undefined
     * }
     *
     * 4.在函式體裡面找函式宣告,值賦予函式體
     *
     * AO{
     *     a:1=>function a(){},
     *     b:undefined,
     *     d:function d(){}
     * }
     * 此時預編譯結束,開始函式執行
     */


根據步驟分析

1、建立AO物件

2、找形參和變數宣告,將變數的形參名作為AO的屬性名,值為undefined

形參:a

變數宣告:a,b

由於形參和變數宣告都是同一個,所以肯定出現覆蓋的情況,取其中一個就可以了,此時AO物件中含有兩個屬性

AO{
 a:undefined,
 b:undefined,
}

3、將實參值和形參進行統一

實參值:1

AO{
 a:1,
 b:undefined
}

此時的a從undefined變成1

4、在函式體裡面找函式宣告,值賦予函式體

函式宣告:function a(){},function d(){}

注意:b是函式表示式,不要弄錯了

AO{
 a:function a(){},
    b:undefined,
    d:function d(){}    
    
}

此時預編譯結束,函式開始執行,解釋一行執行一行

第一個輸出function a(){},到了第二個的時候,var a已經提升了,但是a=123沒有呼叫,所以第二個輸出123

第三個的時候,function a(){}已經進行提升了,所以這一行程式碼不要看,直接輸出123,同理b=function (){}賦值也一樣。

答案依次是:function a(){},123,123,function (){}

例二

    function test(a,b{
        console.log(a);
        c=0;
        var c;
        a=3;
        b=2;
        console.log(b);
        function b({}
        function d({}
        console.log(b);
    }
    test(1);
    /***
     * 1.建立AO物件
     * AO{
     *
     * }
     * 2.找形參和變數宣告,將變數的形參名作為AO的屬性名,值為undefined
     *  AO{
     *     a:undefined,
     *     b:undefined,
     *     c:undefined,
     *  }
     * 3.將實參值和形參進行統一
     *  AO{
     *      a:undefined=>a:1,
     *      b:undefined,
     *      c:undefined
     *  }
     *  4.在函式體裡面找函式宣告,值賦予函式體
     *  AO{
     *      a:1,
     *      b:undefined=>function b(){},
     *      c:undefined,
     *      d:function d(){}
     *  }
     *  預編譯階段結束,函式開始執行
     *
     */


1、建立AO物件

2、找形參和變數宣告,將變數的形參名作為AO物件的屬性名,值為undefined

形參:a,b

變數宣告:c

AO{
 a:undefined,
    b:undefined,
    c:undefined,
}

3、將實參值和形參進行統一

實參值:1

AO{
    a:1,
    b:undefined,
    c:undefined,
}

4、在函式體裡面找函式宣告,值賦予函式體

函式宣告:function b(){},function d(){}

AO{
    a:1,
    b:function b(){},
    c:undefined,
    d:functiond(){}
}

預編譯階段結束,函式開始執行

答案:1,2,2

注意:函式執行上下文發生在函式執行的前一刻

全域性預編譯(全域性執行期上下文)

to be honest,全域性預編譯和函式預編譯都差不多,少了中間的第三步,將實參值和形參進行統一

  • 建立GO物件
  • 找變數宣告,值為undefined
  • 找函式宣告,值賦予函式體

函式預編譯發生在函式執行的前一刻,而全域性預編譯發生在script標籤建立的時候,可以看作script是一個大的function。

全域性執行上下文和函式執行上下文同時使用才是真香,一起來看如下兩個示例

例一

    console.log(test);
    function test(test{
        console.log(test);
        var test=234;
        console.log(test)
        function test({}
    }
    test(1);
    var test=123;

    /***
     * 1.建立GO物件
     * GO{
     *
     * }
     * 2.找變數宣告,值為undefined
     * GO{
     *     test:undefined
     * }
     * 3.找函式宣告,值賦予函式體
     * GO{
     *     test:undefined=>function test(){....此處省略},
     * }
     * 全域性預編譯結束,開始執行程式碼
     *
     */


    /***
     * 1.建立AO物件
     * AO{
     *
     * }
     * 2.找形參和變數宣告,將形參的變數名作為AO物件的屬性名,值為undefined,
     *  AO{
     *     test:undefined,
     *  }
     *  3.將實參值和形參值統一
     *  AO{
     *      test:undefined=>test:1,
     *  }
     *  4.在函式體裡面找函式宣告,值賦予函式體
     *  AO{
     *      test:1=>test:function test(){}
     *  }
     *  函式預編譯結束,開始執行程式碼
     *
     *
     *
     */


一、全域性預編譯

1、建立GO物件

GO{

}

2、找變數宣告,值為undefined

變數宣告:test

GO{
 test:undefined
}

3、找函式宣告,值賦予函式體

函式宣告:function test(test){....}

GO{
    test:function test(test){....}
}

全域性預編譯結束,開始執行程式碼,進入函式預編譯

二、函式預編譯

1、建立AO物件

AO{
    
}

2、找形參和變數宣告,將形參的變數名作為AO的屬性名,值為undefined

形參:test

變數宣告:test

AO{
    test:undefined
}

3、將實參值和形參統一

實參值:1

AO{
    test:1
}

4、在函式體裡找函式宣告,值賦予函式體

函式宣告:function test(){}

AO{
    test:function test(){}
}

函式預編譯結束,開始執行程式碼

答案:function test(test){....},function test(){},234

這裡需要多分析全域性預編譯,還是萬變不離其中

例二

    global=100;
    function fn({
        console.log(global);
        global=200;
        console.log(global);
        var global=300;
    }
    fn();
    var global;

    /***
     * 1.建立GO物件
     * GO{
     *
     * }
     * 2.找變數宣告,值為undefined
     * GO{
     *     global:undefined
     * }
     * 3.找函式宣告,值賦予函式體
     * GO{
     *     global:undefined,
     *     fn:function fn(){...}
     * }
     *
     * 全域性預編譯結束,執行程式碼,進入函式預編譯
     */


    /***
     *1.建立AO物件
     * AO{
     *
     * }
     * 2.找形參和變數宣告,將形參的變數名作為AO的屬性名,值為undefined
     *  AO{
     *      global:undefined
     *  }
     * 3.將實參值和形參統一
     *  AO{
     *      global:undefined
     *  }
     * 4.在函式體裡面找函式宣告,值賦予函式體
     * AO{
     *     global:undefined
     * }
     *函式預編譯結束,開始執行程式碼
     *
     */

一、全域性預編譯

1、建立GO物件

GO{
    
}

2、找變數宣告,值為undefined

變數宣告:global

GO{
    global:undefined
}

3、找函式宣告,值賦予函式體

GO{
    global:undefined,
    fn:function fn(){.....}
}

全域性預編譯完成,執行程式碼,進入函式預編譯

二、函式預編譯

1、建立AO物件

AO{
    
}

2、找形參和變數宣告,將形參的變數名作為AO物件的屬性名,值為undefined

形參:沒有

變數宣告:global

AO{
    global:undefined
}

3、將實參值和形參統一

實參值:沒有

AO{
    global:undefined
}

4、在函式體裡找函式宣告,值賦予函式體

函式宣告:沒有

AO{
    global:undefined
}

函式預編譯完成,執行程式碼

答案:undefined,200

在這裡涉及一點點作用域和作用域鏈的知識,就近原則,自己有就用自己的,自己沒有就看下GO裡面是否有,如果都沒有就是undefined,

函式執行的時候,自己裡面有global,所以就用自己的。

現在回過頭來看現在這兩句話

  • 1、imply global 暗示全域性變數,即任何變數,如果變數未經宣告就賦值,此變數就為全域性物件所有

  • 2、一切宣告的全域性變數都是window屬性

可以確認的是GO就是window,從window裡面取值就是從GO裡面取值。

經典題目

筆者找了一道非常有意思的題目,自己按照步驟也做錯了,所以在這裡分享一下給大家

    a=100;
    function demo(e{
        function e({}
        arguments[0]=2;
        console.log(e);
        if(a){
            var b=123;
            function c({}
        }
        var c;
        a=10;
        var a;
        console.log(b);
        f=123;
        console.log(c);
        console.log(a);
    }
    var a;
    demo(1);
    console.log(a);
    console.log(f);
    /***
     * 1.建立GO物件
     * GO{
     *
     * }
     * 2.著變數宣告,值為undefined
     * GO{
     *     a:undefined
     * }
     * 3.找函式宣告,值賦予函式體
     * GO{
     *     a:undefined,
     *     demo:function demo(e){...}
     * }
     * 全域性預編譯完成,執行程式碼進入函式預編譯
     *
     */

    /***
     *1.建立AO物件
     * AO{
     *
     * }
     * 2.找形參和變數宣告,將形參的變數名作為AO物件的屬性名,值為undefined
     *  AO{
     *      e:undefined,
     *      a:undefined,
     *      b:undefined,
     *      c:undefined,
     *
     *  }
     *  3.將實參值和新參統一
     *  AO{
     *      e:1,
     *      a:undefined,
     *      b:undefined,
     *      c:undefined,
     *  }
     *  4.在函式體裡面找函式宣告,值賦予函式體
     *  AO{
     *      e:function e(){},
     *      a:undefined,
     *      b:undefined,
     *      c:function c(){}
     *  }
     *  函式預編譯完成,執行程式碼
     */

一、全域性預編譯

1、建立GO物件

GO{

}

2、找變數宣告,值為undefined

變數宣告:a

GO{
    a:undefined
}

3、找函式宣告,值賦予函式體

函式宣告:function demo(e){...}

GO{
    a:undefined,
    demo:function demo(e){...}
}

全域性預編譯完成,執行程式碼,進入函式預編譯

二、函式預編譯

1、建立AO物件

AO{
    
}

2、找形參和變數宣告,將形參的變數名作為AO物件的屬性名,值為undefined

形參:e

變數宣告:a,b,c

AO{
    e:undefined,
    a:undefined,
    b:undefined,
    c:undefined
}

3、將實參值和形參統一

實參值:1

形參:e

AO{
    e:1,
    a:undefined,
    b:undefined,
    c:undefined
}

4、在函式體裡面找函式宣告,值賦予函式體

函式宣告:function e(){},function c(){}

AO{
    e:function e(){},
    a:undefined
    b:undefined
    c:function c(){}
}

函式預編譯完成,執行程式碼

理想答案

2,undefined,function c(){},10,100,123

實際答案

2,undefined,undefined, 10,100 ,123

由於谷歌瀏覽器的新規定,不能在if語句裡面定義函式,所以,function c(){}無法提升

致謝讀者

看到這裡的讀者都是最帥的,最美的,本篇是我嘗試寫文章的新做法,第一次搞這種型別的文章,因為希望自己的文章閱讀起來可以沒有那麼枯燥(可以從中獲取更多的快樂),學習是一件特別痛苦的事情,看文章也是,我也希望自己的文章可以更加的生動、幽默。讓看文章的你既可以學到東西也不會那麼枯燥。

如果您有更好的建議,請在下方留下您寶貴的評論。

結尾

如果覺得本篇文章對您有用的話,可以麻煩您幫忙點亮那個點贊按鈕嗎? JS進階系列-JS執行期上下文(一)

對於二郎神楊戩這個暖男來說:真的真的非常有用,您的支援將是我繼續寫文章前進的動力,我們下篇文章見。

相關文章