對於Javascript 執行上下文的理解

来源:http://www.cnblogs.com/nana-share/archive/2017/09/04/7474985.html
-Advertisement-
Play Games

轉載無源頭地址 在這篇文章中,將比較深入地闡述下執行上下文 – JavaScript中最基礎也是最重要的一個概念。相信讀完這篇文章後,你就會明白javascript引擎內部在執行代碼以前到底做了些什麼,為什麼某些函數以及變數在沒有被聲明以前就可以被使用,以及它們的最終的值是怎樣被定義的。 什麼是執行 ...


 

轉載無源頭地址

 

在這篇文章中,將比較深入地闡述下執行上下文 – JavaScript中最基礎也是最重要的一個概念。相信讀完這篇文章後,你就會明白javascript引擎內部在執行代碼以前到底做了些什麼,為什麼某些函數以及變數在沒有被聲明以前就可以被使用,以及它們的最終的值是怎樣被定義的。

什麼是執行上下文

Javascript中代碼的運行環境分為以下三種:

  • 全局級別的代碼 – 這個是預設的代碼運行環境,一旦代碼被載入,引擎最先進入的就是這個環境。
  • 函數級別的代碼 – 當執行一個函數時,運行函數體中的代碼。
  • Eval的代碼 – 在Eval函數內運行的代碼。

在網上可以找到很多闡述作用域的資源,為了使該文便於大家理解,我們可以將“執行上下文”看做當前代碼的運行環境或者作用域。下麵我們來看一個示例,其中包括了全局以及函數級別的執行上下文:

上圖中,一共用4個執行上下文。紫色的代表全局的上下文;綠色代表person函數內的上下文;藍色以及橙色代表person函數內的另外兩個函數的上下文。註意,不管什麼情況下,只存在一個全局的上下文,該上下文能被任何其它的上下文所訪問到。也就是說,我們可以在person的上下文中訪問到全局上下文中的sayHello變數,當然在函數firstName或者lastName中同樣可以訪問到該變數。

至於函數上下文的個數是沒有任何限制的,每到調用執行一個函數時,引擎就會自動新建出一個函數上下文,換句話說,就是新建一個局部作用域,可以在該局部作用域中聲明私有變數等,在外部的上下文中是無法直接訪問到該局部作用域內的元素的。在上述例子的,內部的函數可以訪問到外部上下文中的聲明的變數,反之則行不通。那麼,這到底是什麼原因呢?引擎內部是如何處理的呢?

執行上下文堆棧

在瀏覽器中,javascript引擎的工作方式是單線程的。也就是說,某一時刻只有唯一的一個事件是被激活處理的,其它的事件被放入隊列中,等待被處理。下麵的示例圖描述了這樣的一個堆棧:

我們已經知道,當javascript代碼文件被瀏覽器載入後,預設最先進入的是一個全局的執行上下文。當在全局上下文中調用執行一個函數時,程式流就進入該被調用函數內,此時引擎就會為該函數創建一個新的執行上下文,並且將其壓入到執行上下文堆棧的頂部。瀏覽器總是執行當前在堆棧頂部的上下文,一旦執行完畢,該上下文就會從堆棧頂部被彈出,然後,進入其下的上下文執行代碼。這樣,堆棧中的上下文就會被依次執行並且彈出堆棧,直到回到全局的上下文。請看下麵一個例子:

    
        (function foo(i) {
            if (i === 3) {
                return;
            }
            else {
                foo(++i);
            }
        }(0));
    

上述foo被聲明後,通過()運算符強制直接運行了。函數代碼就是調用了其自身3次,每次是局部變數i增加1。每次foo函數被自身調用時,就會有一個新的執行上下文被創建。每當一個上下文執行完畢,該上上下文就被彈出堆棧,回到上一個上下文,直到再次回到全局上下文。真個過程抽象如下圖:

由此可見 ,對於執行上下文這個抽象的概念,可以歸納為以下幾點:

  • 單線程
  • 同步執行
  • 唯一的一個全局上下文
  • 函數的執行上下文的個數沒有限制
  • 每次某個函數被調用,就會有個新的執行上下文為其創建,即使是調用的自身函數,也是如此。

執行上下文的建立過程

我們現在已經知道,每當調用一個函數時,一個新的執行上下文就會被創建出來。然而,在javascript引擎內部,這個上下文的創建過程具體分為兩個階段:

  1. 建立階段(發生在當調用一個函數時,但是在執行函數體內的具體代碼以前)
    • 建立變數,函數,arguments對象,參數
    • 建立作用域鏈
    • 確定this的值
  2. 代碼執行階段:
    • 變數賦值,函數引用,執行其它代碼

實際上,可以把執行上下文看做一個對象,其下包含了以上3個屬性:

    
          (executionContextObj = {
            variableObject: { /* 函數中的arguments對象, 參數, 內部的變數以及函數聲明 */ },
            scopeChain: { /* variableObject 以及所有父執行上下文中的variableObject */ },
            this: {}
          }
    

建立階段以及代碼執行階段的詳細分析

確切地說,執行上下文對象(上述的executionContextObj)是在函數被調用時,但是在函數體被真正執行以前所創建的。函數被調用時,就是我上述所描述的兩個階段中的第一個階段 – 建立階段。這個時刻,引擎會檢查函數中的參數,聲明的變數以及內部函數,然後基於這些信息建立執行上下文對象(executionContextObj)。在這個階段,variableObject對象,作用域鏈,以及this所指向的對象都會被確定。

上述第一個階段的具體過程如下:

  1. 找到當前上下文中的調用函數的代碼
  2. 在執行被調用的函數體中的代碼以前,開始創建執行上下文
  3. 進入第一個階段-建立階段:

    • 建立variableObject對象:
      1. 建立arguments對象,檢查當前上下文中的參數,建立該對象下的屬性以及屬性值
      2. 檢查當前上下文中的函數聲明:

        每找到一個函數聲明,就在variableObject下麵用函數名建立一個屬性,屬性值就是指向該函數在記憶體中的地址的一個引用

        如果上述函數名已經存在於variableObject下,那麼對應的屬性值會被新的引用所覆蓋。

    • 初始化作用域鏈
    • 確定上下文中this的指向對象
  4. 代碼執行階段:

    執行函數體中的代碼,一行一行地運行代碼,給variableObject中的變數屬性賦值。

下麵來看個具體的代碼示例:

    
        function foo(i) {
            var a = 'hello';
            var b = function privateB() {
        
            };
            function c() {
        
            }
        }
        
        foo(22);
    

在調用foo(22)的時候,建立階段如下:

    
        fooExecutionContext = {
            variableObject: {
                arguments: {
                    0: 22,
                    length: 1
                },
                i: 22,
                c: pointer to function c()
                a: undefined,
                b: undefined
            },
            scopeChain: { ... },
            this: { ... }
        }
    

由此可見,在建立階段,除了arguments,函數的聲明,以及參數被賦予了具體的屬性值,其它的變數屬性預設的都是undefined。一旦上述建立階段結束,引擎就會進入代碼執行階段,這個階段完成後,上述執行上下文對象如下:

    
        fooExecutionContext = {
            variableObject: {
                arguments: {
                    0: 22,
                    length: 1
                },
                i: 22,
                c: pointer to function c()
                a: 'hello',
                b: pointer to function privateB()
            },
            scopeChain: { ... },
            this: { ... }
        }
    

我們看到,只有在代碼執行階段,變數屬性才會被賦予具體的值。

局部變數作用域提升的緣由

在網上一直看到這樣的總結: 在函數中聲明的變數以及函數,其作用域提升到函數頂部,換句話說,就是一進入函數體,就可以訪問到其中聲明的變數以及函數。這是對的,但是知道其中的緣由嗎?相信你通過上述的解釋應該也有所明白了。不過在這邊再分析一下。看下麵一段代碼:

    
        (function() {
            console.log(typeof foo); // function pointer
            console.log(typeof bar); // undefined
        
            var foo = 'hello',
                bar = function() {
                    return 'world';
                };
        
            function foo() {
                return 'hello';
            }
        
        }());​
    

上述代碼定義了一個匿名函數,並且通過()運算符強制理解執行。那麼我們知道這個時候就會有個執行上下文被創建,我們看到例子中馬上可以訪問foo以及bar變數,並且通過typeof輸出foo為一個函數引用,bar為undefined。

為什麼我們可以在聲明foo變數以前就可以訪問到foo呢?

因為在上下文的建立階段,先是處理arguments, 參數,接著是函數的聲明,最後是變數的聲明。那麼,發現foo函數的聲明後,就會在variableObject下麵建立一個foo屬性,其值是一個指向函數的引用。當處理變數聲明的時候,發現有var foo的聲明,但是variableObject已經具有了foo屬性,所以直接跳過。當進入代碼執行階段的時候,就可以通過訪問到foo屬性了,因為它已經就存在,並且是一個函數引用。

為什麼bar是undefined呢?

因為bar是變數的聲明,在建立階段的時候,被賦予的預設的值為undefined。由於它只要在代碼執行階段才會被賦予具體的值,所以,當調用typeof(bar)的時候輸出的值為undefined。

好了,到此為止,相信你應該對執行上下文有所理解了,這個執行上下文的概念非常重要,務必好好搞懂之!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • calc()對大家來說,或許很陌生,不太會相信calc()是css中的部分。因為看其外表像個函數,既然是函數為何又出現在CSS中呢?這一點也讓我百思不得其解,今天有一同事告訴我,說CSS3中有一個屬性能實現自適應的佈局,首先讓我想到的是box-sizing,但跟我說還可以計算,這讓我不得不想起cal ...
  • 與大家分享一下最近所接觸的面試題 1.對WEB標準以及W3C的理解與認識 標簽閉合、標簽小寫、不亂嵌套、提高搜索機器人搜索幾率、使用外 鏈css和js腳本、結構行為表現的分離、文件下載與頁面速度更快、內容能被更多的用戶所訪問、內容能被更廣泛的設備所訪問、更少的代碼和組件,容易維 護、改版方便,不需要 ...
  • //獲取非行間樣式的方法function getCss(obj,arr){ if(obj.currentStyle){ return obj.currentStyle[arr]; }else{ return getComputedStyle(obj,false)[arr]; }};//獲取class ...
  • 模擬心的跳動 ...
  • 所謂組合模式,就是把一堆結構分解出來,組成在一起,現實中很多這樣的例子,如: 1、肯德基套餐就是一種組合模式, 比如雞腿堡套餐,一般是是由一個雞腿堡,一包薯條,一杯可樂等組成的 2、組裝的台式機同理,由主板,電源,記憶體條,顯卡, 機箱,顯示器,外設等組成的 把一個成型的產品組成部件,分成一個個獨立的 ...
  • 使用zTree插件實現樹形圖中,需要獲取當前點擊的父節點的子節點數的需求,使用treeNode.children獲取子節點數據集合,使用length方法獲取集合長度。將當前節點的treeNode傳入即可調用。/*查找當前節點下一級的子節點數*/function findNodes(treeNode) ...
  • 正則表達式(regular expression)是一個描述字元模式的對象。使用JavaScript正則表達式可以進行強大的模式匹配和文本檢索與替換功能。 手機號碼正則表達式驗證。 或者 感謝 丐幫流寇 的提醒,我查了一下瞭解了“ 小括弧就是括弧內看成一個整體 ,中括弧就是匹配括弧內的其中一個”· ...
  • 1、父組件向子組件傳遞參數 2、子組件向父組件傳遞參數 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...