js–執行上下文和作用域相關問題

前言

  如果你是或者你想成為一名合格的前端開發工作者,你必須知道JavaScript代碼在執行過程,知道執行上下文、作用域、變量提升等相關概念,並且熟練應用到自己的代碼中。本文參考了你不知道的JavaScript,和JavaScript高級程序設計,以及部分博客。

正文

 1.JavaScript代碼的執行過程相關概念

  js代碼的執行分為編譯器的編譯和js引擎與作用域執行兩個階段,其中編譯器編譯的階段(預編譯階段)分為分詞/詞法分析、解析/語法分析、代碼生成三個階段。      

     (1)在分詞/詞法分析階段,編譯器負責將代碼進行分割處理,將語句分割成詞法單元流/數組;

     (2)在解析/詞法分析階段,將上一階段的詞法單元流轉換成由元素嵌套組成的符合程序語法結構的抽象語法樹;

     (3)在代碼生成階段,將抽象語法樹轉換成可執行代碼,並交付給js引擎。

   js代碼執行的三個重要角色:

    (1)js引擎:負責代碼執行的整個過程

    (2)編譯器:負責js代碼語法解析和生成可執行代碼

    (3)作用域:手機並維護所有聲明標識符,根據特定規則確定當前代碼對聲明的標識符的訪問權限

  2. 執行上下文和執行棧

  每當js代碼在運行的時候,它都是在執行上下文中運行。說到執行上下文,需要知道什麼時執行棧,執行棧,就是其他編程語言中的「調用棧」,是一種擁有LIFO(後進先出)數據結構的棧,被用來存儲代碼運行時所創建的執行上下文。當js引擎第一次遇到要執行的代碼的時候,首先會創建一個全局的執行上下文並壓入當前執行棧,每當引擎遇到一個函數調用,它會為該函數創建一個新的執行上下文並壓入棧頂,js引擎執行棧頂的函數,當該函數執行完畢,執行上下文從棧中彈出,控制流程到達下一個上下文。對於每一個執行上下文都含有三個重要屬性:變量對象,作用域鏈,this。這些屬性也需要徹底理解。

  2.1 、上下文調用棧

    var scope1 = "global scope";
        function checkscope1(){
            var scope1 = "local scope";
            function f(){
                console.log(scope1); 
            }
            return f();
        }
        checkscope1();
    var scope2 = "global scope";
        function checkscope2(){
            var scope2 = "local scope";
            function f(){
                console.log(scope2);
            }
            return f;
        }
        checkscope2()();

  上面兩段代碼都會輸出 local scope

    上面代碼中scope一定是局部變量,查找塊級作用域即可,不管何時何地執行 f(),這種綁定在執行f()時依然有效。出現了一樣的結果,但是兩段代碼的執行上下文棧的變化不一樣 :

  第一段代碼:push(<checkscope1>functionContext)=>push(<f>functionContext)=>pop()=>pop()

    第二段代碼:push(<checkscope2>functionContext)=>pop()=>push(<f>functionContext)=>pop()

  2.2 、三種執行上下文類型

  (1)全局上下文

    js引擎開始解析js代碼的時候首先遇到的就是全局代碼,初始化的時候會在調用棧中壓入一個全局執行的上下文,當整個應用程序結束的時候才會清空執行上下文棧,棧的最底部永遠時全局執行上下文。這是默認的或者說基礎的全局作用域,任何函數內部的代碼都在全局作用域中,首先創建一個全局的window對象,然後設置this的值等於這個全局對象,一個程序中只有一個全局執行上下文。在頂層js代碼中可以使用this引用全局對象,因為全局對象時是域鏈的頭,意味着所有非限定性的變量和函數都作為該對象的函數來查詢。

    總之,全局執行上下文只有一個,在客戶端中一般由瀏覽器創建,也就是我們熟知的window對象,我們能通過this直接訪問到它。

  (2)函數上下文

    每當一個函數被調用是,都會外該函數創建一個新的上下文,每個函數都擁有自己的上下文,不過是在函數調用的時候創建的,需要注意的是同一個函數被多次調用,都會創建一個新的上下文。

  (3)eval和with上下文

    執行在 eval和with 函數內部的代碼也會有它屬於自己的執行上下文,但由於 JavaScript 開發者並不經常使用 eval,所以在這裡我不會討論它。

  2.3 、執行上下文創建階段

    執行上下文創建分為創建階段與執行階段兩個階段

    js引擎在執行上下文創建階段主要負責三件事:確定this==>創建詞法環境組件==>創建變量環境組件(目前還不太理解)

    (1)確定this,這個不做詳解

    (2)創建詞法環境組件

    詞法環境是一種規範類型,基於 ECMAScript 代碼的詞法嵌套結構來定義標識符和具體變量和函數的關聯。一個詞法環境由環境記錄器和一個可能的引用外部詞法環境的空值組成。其中環境記錄用於存儲當前環境中的變量和函數聲明的實際位置;外部環境引入記錄很好理解,它用於保存自身環境可以訪問的其它外部環境,那麼說到這個,是不是有點作用域鏈的意思?

    詞法環境有兩種類型:

    • 全局環境(在全局執行上下文中)是沒有外部環境引用的詞法環境。全局環境的外部環境引用是 null。它擁有內建的 Object/Array/等、在環境記錄器內的原型函數(關聯全局對象,比如 window 對象)還有任何用戶定義的全局變量,並且 this的值指向全局對象。
    • 在函數環境中,函數內部用戶定義的變量存儲在環境記錄器中。並且引用的外部環境可能是全局環境,或者任何包含此內部函數的外部函數。

    (3)創建變量環境組件

    變量環境可以說也是詞法環境,它具備詞法環境所有屬性,一樣有環境記錄與外部環境引入。在ES6中唯一的區別在於詞法環境用於存儲函數聲明與let const聲明的變量,而變量環境僅僅存儲var聲明的變量。

  3. JavaScript作用域和作用域鏈

  3.1、作用域

  詞法作用域是在寫代碼或者定義的時候確定的,而動態作用域是在運行時確定的,(this也是)詞法作用域關注函數在何處聲明,而動態作用域關注函數從何處調用,JavaScript採用詞法作用域,其作用域由你在寫代碼是將變量和塊作用域寫在哪裡決定,因此當詞法分析器處理代碼時會保持作用域不變。可以理解為作用域就是一個獨立的地盤,讓變量不會外泄、暴露出去。也就是說作用域最大的用處就是隔離變量,不同作用域下同名變量不會有衝突。

  理解作用域之前先來看一道題

        function foo() {
            console.log(value);
        }
        var value = 1;
        function bar() {
            var value = 2;
            console.log(value);
            foo();
        }
        bar();    

  上面的代碼會輸出什麼呢,首先在全局上下文中聲明foo()函數、value變量(其值為undefined)、bar()函數,代碼執行階段,bar函數上下文入棧並執行,打印出value為2,然後執行foo(),foo()入棧,打印value時找不到該變量,js引擎會查找上層作用域,即全局作用域,於是打印出1。後面函數執行完畢上下文出棧。再來看下面這個函數,作用域是分層的,內層作用域可以訪問外層作用域的變量,反之則不行

  ES6以來,js中的作用域分為全局作用域,函數作用域,塊級作用域和欺騙作用域。

  3.1.1、全局作用域

  在代碼中任何地方都能訪問到的對象擁有全局作用域,最外層函數和在最外層函數外面定義的變量擁有全局作用域,所有末定義直接賦值的變量自動聲明為擁有全局作用域。

  3.1.2、函數作用域

  函數作用域的含義是指,屬於這個函數的全部變量都可以在整個函數的範圍內使用及復用(事實上在嵌套的作用域中也可以使用);
      這個原則是指在軟件設計中,應該最小限度地暴露必 要內容,而將其他內容都「隱藏」起來;
      函數表達式可以是匿名的, 而函數聲明則不可以省略函數名。
  3.1.3、塊作用域
  塊作用域,通常指 { .. } 內部
        (1)if 、 try/catch創建塊作用域;
        (2)let 關鍵字可以將變量綁定到所在的任意作用域中(通常是 { .. } 內部);
        (3)for 循環頭部的 let 不僅將 i 綁定到了 for 循環的塊中,事實上它將其重新綁定到了循環的每一個迭代中,確保使用上一個循環迭代結束時的值重新進行賦值;
        (4)const同樣可以用來創建塊作用域變量,但其值是固定的 (常量)。創建對象時值可以被改變。
  3.1.4、欺騙詞法作用域的方法,eval()和with()
    eval()參數為一個字符串,並把裏面的內容當作書寫在該位置的代碼一樣處理(非嚴格模式);
   with()當需要重複引用一個對象的多個屬性時,可以不需要重複引用對象本身。

  3.2、作用域鏈

    作用域鏈本質上就是根據名稱查找變量(標識符名稱)的一套規則。規則非常簡單,在自己的變量對象里找不到變量,就上父級的變量對象查找,當抵達最外層的全局上下文中,無論找到還是沒找到,查找過程都會停止。查找會在找到第一個匹配的變量時停止,被稱為遮蔽效應

   作用域鏈的用途是保證對執行環境有權訪問的所有變量和函數的有序訪問                              
        作用域鏈:當函數定義時,系統生成([scope])屬性,該屬性保存該函數的作用域鏈,該作用域鏈的第0位存儲當前環境下的全局執行期上下文GO,GO里存儲全局下的所有對象,其中包含函數和全局變量,當函數執行的前一刻,預編譯的時候,作用域鏈的頂端(第0位)存儲函數生成的執行上下文AO,同時第一位存儲GO
        查找變量是到函數存儲的作用域鏈中從頂端開始依次向下查找(函數內部作用域在最頂端,證明了函數可以訪問外部的變量,而外部無法訪問函數內部的變量)
  4.執行上下文和作用域的區別
  每個函數調用都有與之相關的作用域和上下文。從根本上說,範圍是基於函數(function-based)而上下文是基於對象(object-based)。換句話說, 作用域是和每次函數調用時變量的訪問有關,並且每次調用都是獨立的。上下文總是關鍵字 this 的值,是調用當前可執行代碼的對象的引用。作用域是函數定義的時候就確定好的了,函數當中的變量是和函數所處的作用域有關,函數運行的作用域也是與該函數定義時的作用域有關。而上下文,主要是關鍵字this的值,這個是由函數運行時決定的,簡單來說就是誰調用此函數,this就指向誰。
  5.最後
  以上就是本文的全部內容,希望給讀者帶來些許的幫助和進步,方便的話點個關注,小白的成長之路會持續更新一些工作中常見的問題和技術點。