1. 程式人生 > 實用技巧 >JavaScript程式碼是怎麼執行的?

JavaScript程式碼是怎麼執行的?

前言 眾所周知,JavaScript是單執行緒語言。所以JavaScript是按順序執行的! 先編譯再執行 變數提升 請看下面的例子:
console.log(cat)
catName("Chloe");
var cat = 'Chloe'
function catName(name) {
    console.log("我的貓名叫 " + name);
}

  

按照得出的結論:"JavaScript是按順序執行的"來看,步驟如下:
  • 執行第一句的時候,cat並沒有定義,結果應該是丟擲一個錯誤,然後結束執行。
Uncaught ReferenceError: cat is not defined

  

但實際的執行結果並不是這樣: 不僅可以執行,catName()執行結果也輸出了。 這種現象就是: 變數提升 從概念的字面意義上說,“變數提升”就是把變數和函式的宣告移動到程式碼的最前面,變數被提升後,會給變數設定預設值--undefined。 調整之後的執行順序如下:
  • 首先執行var cat = undefined和function catName(){}
  • 然後執行console.log(cat) // undefined
  • 接著呼叫catName()
  • 最後給cat賦值cat = 'Chloe'
移動一詞容易造成誤解。實際在物理層面上程式碼的位置並沒有改變。JavaScript是解析執行的語言,在執行前會先經過編譯階段。造成這種現象的原因是:JavaScript引擎在編譯階段中將變數和函式的宣告放在了記憶體中。
執行上下文
變數提升(Hoisting)被認為是, Javascript中執行上下文 (特別是建立和執行階段)工作方式的一種認識
在編譯階段,JavaScript會為上述程式碼建立一個執行上下文和可執行程式碼。 執行上下文是JavaScript執行一段程式碼時的執行環境,包含this、變數、物件以及函式等。 1、在編譯階段
  • JavaScript引擎會將var變數宣告和函式宣告等的變數提升內容放在變數環境中。
  • 接下來JavaScript引擎會把宣告以外的程式碼編譯為位元組碼--可執行程式碼。
2、執行階段
  • 執行到console.log(cat)時,JavaScript引擎在變數環境中查詢cat這個變數,由於變數環境存在cat變數,並且其值為undefined,所以這時候就輸出undefined。
  • 當執行到catName函式時,引擎在變數環境中查詢該函式,由於變數環境中存在該函式的引用,所以引擎執行該函式,並輸出執行結果。
  • 執行cat賦值,引擎在變數環境查詢到cat變數,並進行賦值。
建立執行上下文的三種情況: 1、全域性執行上下文:JS引擎在編譯全域性程式碼時,建立全域性執行上下文。在當前頁面中,全域性執行上下文僅有一個。 2、函式執行上下文:在呼叫一個函式時,JS引擎會建立一個函式執行上下文。一般情況下,當函式執行完畢後就會銷燬此函式執行上下文。 3、eval函式執行上下文:執行eval函式時,也會建立一個執行上下文。 呼叫棧 JS引擎通過棧的資料結構來管理多個執行上下文。
棧是電腦科學中的一種抽象資料型別,只允許在有序的線性資料集合的一端(稱為堆疊頂端,英語:top)進行加入資料(英語:push)和移除資料(英語:pop)的運算。因而按照後進先出(LIFO, Last In First Out)的原理運作
在一個執行上下文建立好後,JS引擎就會它壓進棧中。管理執行上下文的棧結構就稱為呼叫棧,或者執行上下文棧。 請看下面例子:
function foo() {
    var a = 0
    console.log(a)
}
function bar() {
    var b = 1
    foo()
    console.log(b)
}
bar()

  


步驟如下: 1、建立全域性執行上下文,並將其壓入棧底。 2、執行全域性程式碼:bar()。呼叫bar函式時,JS引擎會編譯bar函式,併為其建立一個函式執行上下文。最後將其執行上下文壓入棧中,並且將變數b賦予預設值undefined。 3、執行bar函式內部的程式碼。先執行b = 1的賦值操作,然後呼叫foo函式。JS引擎編譯foo函式,併為其建立一個函式執行上下文。最後將其執行上下文壓入棧中,並且將變數a賦予預設值undefined。 4、執行foo內部的程式碼。執行a = 1賦值操作,然後輸出a的值。foo函式執行完畢後,呼叫棧就將其執行上下文從棧頂彈出。接著執行bar函式。 5、執行完bar函式後,呼叫棧就將其執行上下文從棧頂彈出。剩下全域性執行上下文 整個JavaScript流程執行就到此結束了。 呼叫棧是JS引擎追蹤函式執行的一個機制,當一次有多個函式被呼叫時,通過呼叫棧就能夠追蹤到哪個函式正在被執行以及各函式之間的呼叫關係。 var缺陷與塊級作用域 變數提升帶來的問題 1、變數被覆蓋
var cat = "foo"
function catName(){
  console.log(cat);
  if(false){
   var cat = "bar"
  }
  console.log(cat);
}
catName()  
呼叫catName時,呼叫棧如下圖所示:
  • 建立catName執行上下文時,JavaScript引擎會將var變數宣告cat提升內容放在變數環境中,賦予預設值undefined。
  • 執行到catName內部的console.log(cat)時,在catName執行上下文中的變數環境找到了cat的值,輸出undefined。
  • if判斷為false,不執行。
  • 執行console.log(cat),參照第二步,輸出undefined。
2、變數沒被銷燬
function foo () {
    for (var i=0; i<10; i++){}
    console.log(i)
}
foo()

  

直觀的來說,會以為for迴圈結束後,i會被銷燬。結果並非如此,console.log(i)輸出10。 原因也是變數提升,在建立foo執行上下文時,i被提升了。所以for迴圈結束後,i並沒有被銷燬。 塊級作用域 儲存變數中的值以及對這個值進行訪問或修改,是程式語言的基本功能。而 作用域 則是如何儲存變數以及如何訪問這些變數的規則。 在ES6前,JavaScript只支援兩種方法建立作用域:
  • 全域性作用域
  • 函式作用域
而其他程式語言則都普遍支援塊級作用域。 塊級作用域 就是使用一對大括號包裹的一段程式碼,比如函式、判斷語句、迴圈語句,甚至單獨的一個{}都可以被看作是一個塊級作用域。 簡單來講,在塊級作用域內部定義的變數在其塊級作用域外部是訪問不到的,並且等該內部程式碼執行完成之後,其定義的變數會被銷燬。 由於JavaScript不支援塊級作用域,所以才會有變數提升帶來的問題。 幸好,ES6改變了現狀,引入了新的let和const關鍵字,提供了除var以外的另一種變數宣告方式。 let和const關鍵字可以將變數繫結到所在的任意作用域中(通常是{}內部)。換句話說,let為其宣告的變數建立了塊作用域。 塊級作用域的作用,請看下面例子:
var cat = "foo"
function catName(){
  if(true){
   var cat = "bar"
   console.log(cat);
  }
  console.log(cat);
}
catName()

  

在這段程式碼中,有兩處聲明瞭cat變數,一處在全域性作用域,一處在catName函式作用域中的if語句裡面。 在執行if語句內部時,呼叫棧如下圖所示: 從圖中可看出兩處console.log(cat)都輸出bar。 使用let改寫上面程式碼
var cat = "foo"
function catName(){
  if(true){
   let cat = "bar"
   console.log(cat);
  }
  console.log(cat);
}
catName()

  

if語句執行結束後,let宣告的cat變數就會被銷燬,第二處的console.log(cat)就會輸出foo JavaScript內部實現塊級作用域 請看下面的例子
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()

  

步驟如下: 1、第一步建立全域性執行上下文 2、執行foo(),建立foo函式的執行上下文
  • 在函式內部使用var宣告的變數都放在變數環境中,並賦予一個預設值undefined。
  • 在函式內部使用let宣告的變數被放在詞法環境中,沒有賦予一個預設值。
  • 在函式內部中的{}內部使用let宣告的變數沒有放在詞法環境中。
3、執行foo函式內部的{}塊,此時a和b的已經初始化了,並且進入作用域塊時,作用域塊中通過let宣告的變數,會被存放在詞法環境的一個單獨的區域中,這個區域中的變數並不影響作用域塊外面的變數。 在詞法環境內部維護了一個棧結構,棧底是函式最外層的變數,進入一個作用域塊後,就會把該作用域塊內部的變數壓入棧中;當作用域執行完成之後,該作用域的let和const宣告的變數就會從棧頂彈出。 4、作用域塊執行結束後,詞法環境的棧結構就把其資訊從棧頂彈出。 使用let或const宣告的變數,在達到宣告處之前都是無法訪問的,試圖訪問會導致一個 引用錯誤,即使在通常是安全的操作時(例如使用typeof運算子)也是如此。示例如 下:
if (true) {
    console.log(typeof value); // 引用錯誤
    let value = 'blue'
}

  

因為value位於暫時性死區(temporal dead zone, TDZ)的區域內--該名稱並沒有在ECMAScript規範中被明確命名,但經常被用於描述let或const宣告的變數為何在宣告之前無法被訪問。 總結 1、JavaScript程式碼是先編譯再執行的。 2、執行是按順序一段一段執行的,一段程式碼是指一個執行上下文。 3、執行上下文有三種情況:
  • 全域性執行上下文
  • 函式執行上下文
  • eval執行上下文
4、let和const支援塊級作用域
作者:zhangwinwin 連結:JavaScript程式碼是怎麼執行的? 來源:github