天天看點

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.最後

  以上就是本文的全部内容,希望給讀者帶來些許的幫助和進步,友善的話點個關注,小白的成長之路會持續更新一些工作中常見的問題和技術點。