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
來源:github


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM