Javascript 執行機制

執行流程

先編譯,再執行。 瀏覽器

JS代碼執行流程

  • 編譯階段:進行變量提高,變量與函數會被存放到變量環境中,變量的默認值被設爲 undefined.若存在兩個相同的函數,最終存放在變量環境中的是後面那個。若是函數帶有參數,編譯過程當中,參數會經過參數列表保存在變量環境中。
  • 執行階段:JS 引擎會從變量環境中去查找自定義的變量和函數。

哪些狀況下代碼在執行以前會編譯並建立執行上下文?

  1. 當 JS 執行全局代碼的時候,會編譯全局代碼並建立全局執行上下文,並且在整個頁面生存週期內,全局執行上下文只有一份。
  2. 當調用一個函數的時候,函數體內的代碼會被編譯,並建立函數執行上下文,通常狀況下,函數執行結束以後,建立的函數執行上下文會被銷燬。
  3. 當使用 eval 的時候,eval 的代碼也會被編譯,並建立執行上下文。

調用棧

  • 棧溢出是如何產生的?
    當調用一個函數時,會給他建立一個執行上下文 push 到棧中,執行完畢從棧中 pop。若函數內部又調用了其餘函數,內部又調用其餘函數...,不斷將執行上下文往棧中 push 卻沒有 pop,超過必定數量就會致使棧溢出報錯。閉包

    沒有終止條件的遞歸會一直建立新函數的執行上下文壓入棧中,超過棧容量的最大先以後就會報錯;app

    能夠經過把遞歸改形成其餘形式、加入定時器拆分任務等方法來解決。函數

    調用棧是 JavaScript 引擎追蹤函數執行的一個機制,當一次有多個函數被調用時,經過調用棧就能追蹤到哪一個函數正在被執行和各個函數間的調用關係。工具

如何用好調用棧?

  • 利用瀏覽器查看調用棧信息
    函數調用關係
  • 加入 console.trace() 輸出當前函數調用關係
    使用 trace 打印調用棧信息

JS 中 let,const,{} 如何實現塊級做用域?

ES6 以前的做用域

  • 全局做用域中的對象在代碼中的任何地方都能訪問,其生命週期伴隨着頁面的生命週期。
  • 函數做用域就是在函數內部定義的變量或者函數只能在函數內部被訪問。函數執行結束以後,函數內部定義的變量被銷燬。

變量提高帶來的問題

  1. 變量容易被覆蓋
chat* myname = "geek time";
void showName() {
  printf("%s \n", myname);  // 'geek time'
  if(0){
    chat* myname = "Hei ha";
  }
}
int main(){
  showName();
  return 0;
}
複製代碼

最終打印爲 'geek time'ui

var myname = "geek time";
function showName() {
  console.log(myname);
  if (0) {
    var myname = "Hei ha";
  }
}
showName();
複製代碼

最終打印爲 undefinedthis

  1. 本應銷燬的變量沒有銷燬
function foo() {
  for (var i = 0; i < 7; i++) {}
  console.log(i);
}
foo(); //7
複製代碼

輸出爲 7,變量 i 在 foo 循環結束後並無被銷燬,說明在建立執行上下文階段,變量 i 就已經被提高了。spa

在其餘語言中,for,if,while,{},函數塊等內部變量執行完後就會被銷燬。設計

ES6 如何解決變量提高帶來的問題?

經過 var 聲明的變量,在編譯階段被放進變量環境,而經過 let,const 聲明的被放進詞法環境(Lexical Environment);
3d

let聲明變量-1
每一個塊級做用域內的 let,const 聲明又被放進詞法環境的一個單獨區域中。
let聲明變量-2
看成用域塊執行結束後,內部定義的變量就會從詞法環境的棧頂彈出,從而實現和其餘語言同樣的變量銷燬。
let聲明變量-3

做用域鏈與閉包

做用域鏈

  • 下面代碼輸出什麼?
function bar() {
  console.log(myName);
}
function foo() {
  var myName = "極客邦";
  bar();
}
var myName = "極客時間";
foo();
複製代碼

執行bar時的調用棧
按上面調用棧順序來分析,那麼結果應該是 極客邦; 實際答案是 極客時間
帶有外部引用的調用棧
每一個執行上下文的環境中都包含了一個外部引用,用來指向外部執行的上下文,上圖中的 outer。

當一段代碼使用一個變量時,JS 引擎首先在「當前執行上下文(bar)」中查找該變量,若沒有,則在 outer 所指向的執行上下文中查找,這個查找鏈條就是做用域鏈

  • 問題:那麼 foo 中調用的 bar,爲何 bar 的外部引用是全局執行上下文而不是 foo 函數的執行上下文?

由於在 JS 執行過程當中,做用域鏈是由詞法做用域決定的。

詞法做用域

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

詞法做用域
上圖中,整個詞法做用域鏈的順序是: foo 函數做用域 -> bar 函數做用域 -> main 函數做用域 -> 全局做用域。

詞法做用域是代碼階段就決定好的,和函數怎麼調用沒有關係。 再看上面的問題,就知道打印的結果爲何是「極客時間」了。 若是換成下面的:

function foo() {
  var myName = "極客邦";
  function bar() {
    console.log(myName);
  }
  return bar();
}
var myName = "極客時間";
foo();
複製代碼

此時打印的就是「極客邦」了。

塊級做用域中的變量查找

function bar() {
  var myName = "瀏覽器";
  let test1 = 100;
  if (1) {
    let myName = "Chrome 瀏覽器";
    console.log(test);
  }
}

function foo() {
  var myName = "極客邦";
  let test = 2;
  {
    let test = 3;
    bar();
  }
}

var myName = "極客時間";
let myAge = 10;
let test = 1;
foo();
複製代碼

結合上面的做用域鏈與詞法做用域,易得最終輸出結果爲 1。 查找順序以下(圖中標記的 1,2,3,4,5)

塊級做用域的變量查找

閉包

function foo() {
  var myName = "極客時間";
  let test1 = 1;
  const test2 = 2;
  var innerBar = {
    getName() {
      console.log(test1);
      return myName;
    },
    setName(newName) {
      myName = newName;
    }
  };
  return innerBar;
}
var bar = foo();
bar.setName("極客邦");
bar.getName();
console.log(bar.getName());
複製代碼

執行到 return innerBar 時的調用棧

根據詞法做用域的規則易得,內部函數 getNamesetName能夠訪問 foo 中的 myName 和 test1。因此,當 foo 執行完後,這兩個變量成爲 foo 閉包的專屬變量,除了 setName 和 getName 其餘任何地方都沒法訪問 foo 閉包中的變量。調用棧的狀態以下:
閉包的產生過程-1
閉包的產生過程

經過上圖能夠看出,當執行到 foo 時,閉包就產生了,foo 結束後,getName 與 setName 都引用了clourse(foo) 對象,因此即便 foo 函數結束了,clourse(foo)依然被其內部的 getName 和 setName 引用,調用這兩個方法時,建立的執行上下文就包含了 clourse(foo)

  • 站在內存模型角度分析代碼的執行流程
  1. 執行 foo 函數,編譯、建立執行上下文。
  2. 編譯過程當中,遇到 setName,發現其中使用了外部函數的變量myName,因而生成一個閉包環境來存放 myName 變量。
  3. 接着掃描,又遇到 getName 發現函數內部有使用了外部變量,JS 引擎又將 test1 存放到閉包中。
  4. test2 沒有被函數內部引用,因此依然保存在執行棧中。
  • 產生閉包的核心兩步:
  1. 預掃描內部函數
  2. 把內部函數引用的外部變量保存到堆中

閉包如何使用?

當執行 bar.setName() 方法中的 myName = 'xxx' 時,JS 引擎會沿着「當前執行上下文 -> foo 函數閉包 -> 全局執行上下文」的屬性來查找,以下:

執行bar.setName時調用棧狀態

Chrome 開發者工具中在 innerBar 的函數中打斷點,刷新頁面也可查看閉包狀態。
開發者工具中閉包展現

經過 Scope便可查看做用域鏈的狀況。

閉包如何回收?

若是引用閉包的函數是全局變量,那麼閉包會一直存在到頁面關閉;但若是這個閉包之後再也不使用的話,就會形成內存泄漏。

若是引用閉包的函數是局部變量,等函數銷燬後,下次 JS 引擎執行垃圾回收時,判斷閉包若是已再也不被使用,就會回收這塊內存。

綜上所述,若閉包一直使用,則做爲全局變量,不然爲局部變量。

this

this 是和執行上下文綁定的,執行上下文有全局、函數、eval 執行上下文,故對應的 this 也有這三種。

執行上下文中的 this

全局執行上下文中的 this

window

函數執行上下文中的 this

  1. 經過 call,bind,apply 設置
let bar = {
  myName: 'x'
}
function foo() {
  this.myName = 'xxx'
}
foo.call(bar)
複製代碼
  1. 經過對象調用方法設置
var myObj = {
  name: 'x',
  showThis() {
    console.log(this)
  }
}
myObj.showThis()  // 等同於 myObj.showThis.call(myObj)
var foo = myObj.shiwThis
foo() // window
複製代碼

在全局環境中調用一個函數,函數內部的 this 指向的是全局變量 window。 經過一個對象來調用其內部的一個方法,該方法的執行上下文中的 this 指向對象自己。
3. 經過構造函數中設置 new 運算符

this 的設計缺陷

  1. 嵌套函數中的 this 不會從外層繼承 this 沒有做用域限制,因此嵌套函數不會從調用它的函數中繼承。
var myObj = {
  name: 'jk',
  showThis() {
    console.log(this) // myObj
    function bar() {
      console.log(this)
    }
    bar() // window
  }
}
複製代碼

解決辦法:1. 外層綁定 this 2. 箭頭函數
2. 普通函數中的 this 默認指向全局對象 window 嚴格模式下,默認執行一個函數,這個函數執行上下文中的 this 是 undefined

some question

  1. 第一題
showName();
var showName = function() {
  console.log(2);
};
function showName() {
  console.log(1);
}
複製代碼

輸出 1,第一個 showName 帶 var 通過變量提高後被賦值爲 undefined,變量 showName 會被下面同名函數覆蓋,再次執行 showName 就爲 2,具體過程以下

// 編譯
var showName = undefined;
function showName() {
  console.log(1);
}
// 執行
showName(); // 1
showName = function() {
  console.log(2);
};
showName(); // 2
複製代碼
  1. 第二題
let myname = "geek time";
{
  console.log(myname);
  let myname = "Hei ha";
}
複製代碼

最終的打印結果不是 undefined.
而是:Cannot access 'myname' before initialization 緣由:在塊級做用域內,let 變量只是建立被提高,初始化並無被提高,在初始化以前使用變量,會造成一個暫時性死區。

  • var 的建立和初始化被提高,賦值不會被提高。
  • let 的建立被提高,初始化和賦值不會被提高。
  • function 的建立、初始化和賦值均會被提高。
  • 暫時性死區:
    執行函數時纔有進行編譯,抽象語法樹(AST)在進入函數階段就生成了,而且函數內部做用域已經明確了,因此進入塊級做用域不會有編譯過程,只不過經過 let 或者 const 聲明的變量會在進入塊級做用域時才被建立,可是在該變量沒有賦值以前,引用該變量 JavaScript 引擎會拋出錯誤---這就是「暫時性死區」

參考資料

瀏覽器工做原理與實踐

相關文章
相關標籤/搜索