和執行上下文有關的那些事

要脫離單純勞動力的角色,就要深刻去了解那些底層的原理,今天咱們就來聊聊執行上下文。只有理解了 JavaScrip 的執行上下文,你才能更好地理解 JavaScript 語言自己,由於JavaScript中的不少概念變量提高、做用域和閉包等等都是和執行上下文有關的。
執行上下文主要包括:javascript

  • 全局執行上下文——代碼首次執行的默認環境。前端

  • 函數執行上下文——每當進入一個函數內部。java

  • Eval執行上下文——eval內部的文本被執行時。面試

咱們就從那個這些概念出發,看看執行上下文的那些事情。瀏覽器

變量提高

變量提高對於前端人員來講是已經習覺得常的事情,但從別的語言來看,這種特性顯得很是奇怪。
所謂的變量提高,是指在 JavaScript 代碼執行過程當中,JavaScript 引擎把變量的聲明部分和函數的聲明部分提高到代碼開頭的「行爲」。變量被提高後,會給變量設置默認值,這個默認值就是咱們熟悉的 「undefined」。性能優化

對於變量提高的過程咱們就不詳細講了,不少面試題都在變着花樣的考察變量、函數的提高。bash

咱們來看下對於JavaScript引擎來講在執行過程當中作了什麼,怎麼實現的變量提高。 一段代碼的執行分爲兩個階段:編譯階段執行階段閉包

是的,JavaScript的執行是須要編譯的,只不過它屬於解釋型語言,編譯過程是在執行以前的很短期以內完成的(具體的編譯原理,後面的文章中會詳細介紹)。app

在編譯階段作了什麼呢?

咱們以一個例子來進行分析:函數

showMyName()
console.log(myname)
var myname = '前端記事本'
function showMyName() {
    console.log('函數 showName 被執行');
}
複製代碼

這段代碼在執行的時候,變量myname和showMyName的函數聲明會被提高。這就是在編譯過程當中處理的。

代碼編譯以後會分爲兩個部分:執行上下文可執行代碼。(變量環境中的函數體其實是存在堆內存中的,變量環境中會建立一個指向這個函數體的變量,咱們這裏圖中不作詳細標註了)。

而執行上下文中又分爲:

  • 變量環境

  • 詞法環境

  • this綁定

咱們能夠看到,被提高的變量都被放在了變量環境中,剩餘的部分則是可執行代碼,在執行階段處理。 過程是怎樣的呢? 第 1 行和第 2 行不是變量或者函數聲明,因此 JavaScript 引擎不會作任何處理;

第 3 行是通過 var 聲明的變量,所以 JavaScript 引擎將在環境對象中建立一個名爲 myname 的屬性,並將其初始化爲undefined;

第 4 行是函數聲明, JavaScript 引擎會將函數定義存儲到堆 (HEAP)中,並在環境對象中建立一個 showName 的屬性,而後將該屬性值指向堆中函數的位置(這裏涉及到JavaScript內存管理,你們能夠去查資料瞭解一下啊)。

而後就是將代碼編譯成字節碼的過程了,這裏就不詳細說了。

下面就是執行階段了

這段代碼的變量被提高以後還剩幾部分,就是咱們要執行的代碼了。

showMyName()
console.log(myname)
myname = '前端記事本'
複製代碼

在執行到showName()時會到變量環境中查找這個函數,由於引擎將對這個函數的引用存在了變量環境中,因此很容易找到這段函數代碼並執行;

在執行 console.log 的時候,會在變量環境中找到 myname 變量,但此時它的值爲 undifined,因此會輸出undifined。
最後是對變量myname的賦值操做,會將變量環境中myname的值由undifined -> '前端記事本'
這就是整個變量提高的過程了,其實經過這個過程咱們也就知道了,若是聲明瞭兩個相同名稱的函數,在執行的時候會怎麼樣了:由於名稱相同,因此變量環境中只會存在一個函數名稱變量,這個變量名稱會指向最後聲明的那個函數。
好了,變量提高講完了,咱們能夠看到對於執行上下文的三個部分,咱們只涉及到了變量環境,那其它兩個部分呢?咱們下面再來看另一個概念,做用域。

做用域

上面在提到變量提高的時候咱們說,對於其它語言,變量提高是一個很奇怪的特性。由於JavaScript 變量提高這種特性,會致使了不少與直覺不符的代碼,這也是 JavaScript 的一個重要設計缺陷。

其中一個很大的影響就是做用域。
做用域是指在程序中定義變量的區域,該位置決定了變量的生命週期。通俗地理解,做用域就是變量與函數的可訪問範圍,即做用域控制着變量和函數的可見性和生命週期。

javascript的做用域主要包括:全局做用域函數做用域。沒有塊級做用域。這就形成了咱們面試中那個經典的for循環輸出i的問題。相信你們對於這種面試題的結果已經很瞭解了。

但如今,咱們從執行上下文的角度分析一下。

對於做用域:

全局做用域中的對象在代碼中的任何地方都能訪問,其生命週期伴隨着頁面的生命週期。
函數做用域就是在函數內部定義的變量或者函數,而且定義的變量或者函數只能在函數內部被訪問。函數執行結束以後,函數內部定義的變量會被銷燬。
咱們說過,變量提高會對做用域的理解產生誤解。

再來分析一個例子。

var myname = " 三木 "
function showName(){
  console.log(myname);
  var myname = " 前端記事本 "
  console.log(myname);
}
showName()
複製代碼

這段代碼輸出是「undifined」和「前端記事本」,爲何呢?你能夠看到這裏有兩個 myname 變量:一個在全局執行上下文中,其值是「三木」;另一個在 showName 函數的執行上下文中,變量被提高了,其值是 undefined。

因此在執行第一個 console.log(myname)時直覺覺得會是「三木」,但實際用的是 showName 函數的執行上下文中的myname,此時的值爲 undefined。執行第二個console.log(myname)時,showName 函數的執行上下文中的 myname 已經被賦值爲「前端記事本」了。

因此經過第一個 myname 的輸出能夠看到變量容易在不被察覺的狀況下被覆蓋掉

後來ES6出現了,推出了let、const關鍵字,使得 javascript 多了塊級做用域。 let 和 const 聲明的變量再也不被提高了。

因此下面這段代碼:

function foo(){
    var a = 1
    let b = 2
    {
      let b = 3
      var c = 4
      let d = 5
      console.log(a)
      console.log(b)
    }
    console.log(b) 
    console.log(c)
    console.log(d)
}   
foo()
複製代碼

咱們前面說過 javaScript 引擎會先對其進行編譯並建立執行上下文,而後再按照順序執行代碼。如今咱們引入了 let 關鍵字,let 關鍵字會建立塊級做用域,那麼 let 關鍵字是如何影響執行上下文的呢?

看下上面的例子,它在編譯以後的 foo 函數執行上下文是這樣的:

另外,函數代碼塊中用 let 定義的 b 和 d 並無出如今foo函數的執行上下文中。 因此能夠看到:

函數內部經過 var 聲明的變量,在編譯階段全都被存放到變量環境裏面了。

經過 let 聲明的變量,在編譯階段會被存放到詞法環境(Lexical Environment)中。

在函數的做用域內部,經過 let 聲明的變量並無被存放到詞法環境中。

而後是執行階段,在執行到代碼塊的時候,會在foo函數的詞法環境中單獨一塊區域存放 b 和 d。

能夠看到在代碼塊外部定義的 b 和內部定義的 b 是獨立分開的。

其實,在詞法環境內部,維護了一個小型棧結構,棧底是函數最外層的變量,進入一個做用域塊後,就會把該做用域塊內部的變量壓到棧頂;看成用域執行完成以後,該做用域的信息就會從棧頂彈出,這就是詞法環境的結構。

須要注意下我這裏所講的變量是指經過 let 或者 const 聲明的變量。

因此這樣在執行下面代碼在查詢變量值的時候,會先從詞法環境的棧頂開始查找,若是找不到就去變量環境中去查找。

由於詞法環境維護了一個小的棧,因此在代碼塊執行完以後, b 和 d 所在的區塊就被彈出銷燬了。

這就是塊級做用域在執行棧的處理過程。

Tips:對於 let 和 const 定義的變量有個「暫時性死區」的概念,咱們執行一個例子。

這就是暫時性死區的報錯,初始化以前沒法訪問tmp。其實變量的整個建立過程包括:建立、初始化、賦值。因此 let 變量不會變量提高的真實狀況是:在塊做用域內,let聲明的變量被提高,但變量只是建立被提高,初始化並無被提高,在初始化以前使用變量,就會造成一個暫時性死區。

  • var 的建立和初始化被提高,賦值不會被提高。

  • let 的建立被提高,初始化和賦值不會被提高。

  • function的建立、初始化和賦值均會被提高。

做用域鏈

看個例子:

function bar() {
    console.log(myName)
}
function foo() {
    var myName = " 三木 "
    bar()
}
var myName = " 前端記事本 "
foo()
複製代碼

我原本覺得結果是:「三木」,結果它輸出的是 " 前端記事本 "。爲何會這樣? 這段代碼的執行棧是這樣的:

按照直覺來講,查找 myName 變量的順序應該是:當前函數執行上下文(bar) -> 外部函數執行上下文(foo) -> 全局執行上下文

但這裏有個做用域鏈的概念。

在每一個執行上下文的變量環境中,都包含了一個外部引用,用來指向外部的執行上下文,咱們把這個外部引用稱爲outer。

因此,真實狀況是:

當一段代碼使用了一個變量時,JavaScript 引擎首先會在「當前的執行上下文」中查找該變量,好比上面那段代碼在查找 myName 變量時,若是在當前的變量環境中沒有查找到,那麼 JavaScript 引擎會繼續在 outer 所指向的執行上下文中查找。

那又是什麼決定outer的指向呢?

那就是:詞法做用域: 詞法做用域就是指做用域是由代碼中函數聲明的位置來決定的,因此詞法做用域是靜態的做用域,經過它就可以預測代碼在執行過程當中如何查找標識符。

JavaScript 做用域鏈是由詞法做用域決定的,詞法做用域是由函數聲明位置決定的,因此上面 bar() 函數在全局環境聲明的,因此 outer 指向是全局執行上下文,而不是 foo 函數的執行上下文。

this

對於剛學習 JavaScript 的同窗來講,this 真的很讓人頭大。而這個 this 的指向就是綁定在第一部分咱們說過執行上下文中的 this綁定 上的。

在函數執行上下文中,this 的值取決於該函數是如何被調用的,而不是函數聲明的位置。

通常對於 this 的調用就分爲幾種:

  • 對象調用內部的函數,該方法的執行上下文中的 this 指向對象自己;

  • call、bind和apply改變方法的 this 指向;

  • new 一個構造函數的時候也是 this 指向改變的過程。

閉包

**函數和對其周圍狀態(lexical environment,詞法環境)的引用捆綁在一塊兒構成閉包(closure)。也就是說,閉包可讓你從內部函數訪問外部函數做用域。 ** 對於下面這段代碼,內部函數showName函數,訪問了外部 foo 函數的變量 myName。

function foo() {
                var myName = " 三木 "
                function showName() {
                    console.log(myName)
                }
                return showName
            }
            var bar = foo();
            bar()
複製代碼

整個調用棧的狀況以下圖:

根據詞法做用域的規則,內部函數 showName 老是能夠訪問它們的外部函數 foo 中的變量,因此返回給全局變量 bar 時,雖然 foo 函數已經執行結束,可是 showName 函數依然可使用 foo 函數中的變量 myName 。因此當 foo 函數執行完成以後,其整個調用棧的狀態以下圖所示:

在瀏覽器中看下是這樣的:

從上圖能夠看出,foo 函數執行完成以後,其執行上下文從棧頂彈出了,可是因爲返回的 showName 方法中使用了 foo 函數內部的變量 myName,因此這個變量依然保存在內存中。就像單獨給showName方法配置了一個數據包,不管在哪裏調用showName函數,都會帶着這個數據包。

由於是專屬於showName函數的數據包,因此除了 showtName 函數以外,其餘任何地方都是沒法訪問該數據包的,咱們就能夠把這個數據包稱爲 foo 函數的閉包。

好了,如今咱們終於能夠給閉包一個正式的定義了。在 JavaScript 中,根據詞法做用域的規則,內部函數老是能夠訪問其外部函數中聲明的變量,當經過調用一個外部函數返回一個內部函數後,即便該外部函數已經執行結束了,可是內部函數引用外部函數的變量依然保存在內存中,咱們就把這些變量的集合稱爲閉包。好比外部函數是 foo,那麼這些變量的集合就稱爲 foo 函數的閉包。

總結

咱們從JavaScript的執行上下文的角度分析了一下咱們常見的幾個概念,總體較少比較粗略,感興趣的同窗能夠本身去深刻研究一下執行上下文,對於咱們分析一些執行原理和處理性能優化是很是有幫助的。

參考文章:
《瞭解JavaScript的執行上下文》
《瀏覽器工做原理與實踐》 - 李兵
《Understanding Execution Context and Execution Stack in Javascript》 - Sukhjinder Arora

若是你但願瞭解更多前端知識,請關注個人公衆號「前端記事本」

相關文章
相關標籤/搜索