Javascript進階1--作用域和閉包

從今天開始,我打算將我學到的js知識進行分享,歡迎大家的討論和補充,有任何不足之處,盡情地提出來吧~😁

作用域介紹

作用域是什麼?

本質上是一套規則,用於確定在<font color=red>何處</font>以及<font color=red>如何查找</font>變量(標識符)。

何處?

作用域是可以嵌套的,引擎從當前作用域開始查找,如果找不到,就會向上一級繼續查找,當抵達到最外層的全局作用域查找後,無論找到還是沒找到,都會停止。

<img src="https://user-gold-cdn.xitu.io...; width = "200" height = "300" align="center" />

如何查找變量?

有以下兩種方式:

  • LHS:賦值操作的<font color=orange>目標</font>是誰;結果不成功的話,有兩種情況:

    • 嚴格模式下:拋出 Reference 異常。
    • 非嚴格模式下,自動隱式地創建一個全局變量。
  • RHS:誰是賦值操作的<font color=orange>源頭</font>;結果不成功會報 Reference 異常。

⚠️注意:只會查找一級標識符,比 如foo.bar.baz,只會試圖找到 foo 標識符,找到後,<font color=orange>對象屬性訪問規則</font>後分別接管對 bar、baz 的屬性訪問。

舉🌰:

function foo(a) {
    console.log(a + b);
}
var b = 2;
foo(3);

引擎:作用域,我需要爲 b 進行 LHS引用,這個你見過嗎?
全局作用域:見過見過!剛纔編譯器聲明它來着,給你。
引擎:謝謝大哥,現在我要把2賦值給 b
引擎:作用域啊,還有個事,我要對 foo 進行 RHS 引用,你見過沒啊?
全局作用域:見過呀,它是個函數,給你。
引擎:好的,我現在執行一下 foo 
引擎:哥啊,我需要對 a 進行 LHS 引用,這個你見過沒?
全局作用域:這個也見過,是編譯器把它聲明成 foo 的一個形參了,拿去吧。
引擎:太棒了,現在我把3賦值給 a 了
引擎:foo 作用域啊,我要對 console 進行 RHS 引用,你見過沒啊?
foo作用域:這我也有,是個內置對象,給你
引擎:你總是那麼給力,現在我要看看這裏有沒有 log(),找到了,是個函數。
引擎:哥,我要對 a 進行 RHS 引用,雖然我記得好像有這個值,但是想讓你幫我確認以下。
foo作用域:好,這個值沒變過,你拿走吧。
引擎: 哥,我還要對 b 進行 RHS 引用,你找找唄
foo作用域:我沒聽過啊,你問問我的上級吧:
引擎:foo 的上級作用域兄弟,你見過 b 沒啊?
全局作用域:見過 b 啊,等於2,拿走不謝!
引擎:真棒,我現在把 a + b ,也就是5,傳遞進 log(...)

作用域的工作模型

主要有兩種:

  • 詞法作用域:由你在書寫代碼時將變量和塊作用域寫在哪裏來決定的。有時候可能會有在代碼運行時“修改”詞法作用域的需求,可以通過以下機制:

    • eval():可以接受一個字符串爲參數,並將其中的內容視爲好像在書寫時就存在於程序中這個位置的代碼。

      function foo(str, a) {
          eval(str);
          console.log(a, b)
      }
      var b = 3;
      foo("var b = 4", 2); // 2, 4  
    • with:通過將一個對象的引用當作作用域來處理,將對象的屬性當作作用域中的標識符來處理,從而創建了一個新的詞法作用域。

      function foo(obj) {
          with(obj) {
              a = 2;
          }
      }
      var o1 = {
          a: 3
      };
      var o2 = {
          b: 3
      };
      foo(o1);
      console.log(o1.a)  //2
      foo(o2)
      console.loh(o2.a)  //undefined;
      console.log(a)  
      //2 在o2中,對a進行LHS引用,沒有找到,
      //在o2中不會創造a這個屬性
      //因爲是非嚴格模式,所以會在全局作用域中創建一個變量 a,並賦值給2
      ⚠️注意:這兩個機制只在非嚴格模式下有效,嚴格模式下會拋出 Reference 錯誤。還會導致性能下降,引擎無法在編譯時對其進行優化,所以會變慢。
  • 動態作用域:

作用域的種類

作用域有三種:

  • 全局作用域:生命週期存在於整個程序內,能被程序中任何函數或者方法訪問,在js中默認是可以被修改的。
  • 局部作用域

    • 函數作用域:函數作用域內,對外是封閉的,從外層的作用域無法直接訪問函數內部的作用域。
    • 塊級作用域:任何一對花括號中的語句集都屬於一個塊,在這之中定義的所有變量在代碼塊外都是不可見的。

#### 函數作用域
在javascript中,定義一個函數有四種方式,分別是:

  • 函數聲明:function 關鍵字出現在聲明中第一個詞,它的調用可以<font color=orange>先於聲明</font>。
  • 函數表達式:在執行到達時創建,並只有<font color=orange>從那時起</font>纔可以用。

     - 匿名函數表達式:省略了函數名
     - 具名函數表達式
  • ES6 中的箭頭函數
  • new Function()

     ⚠️注意:函數聲明和函數表達式最大的區別就是他們的名稱標識符將會被綁在何處。
     ```
     var a = 2;
     // 函數聲明,被綁定在所在作用域中,可以直接通過 foo() 來調用它
     function foo() {
         var a = 3;
         console.log(a); //3
     }
     foo();
     // 函數表達式,foo2被綁定在函數表達式自身的函數中,而不是所在的作用域中
     // 也就是,只能在函數內部裏被訪問foo2,外部作用域內不能訪問
     (function foo2() {
         var a = 3;
         console.log(a)  //3
     }
     )()
     console.log(a)  //2
     ```
    

在函數表達式中的立即執行函數表達式(IIFE)使我們不用主動調用函數,它會自己調用,對於做模塊化、處理組件是非常有用的,IIFE一般使用匿名函數表達式。

⚠️注意:調用函數最簡單的方法就是加一對小括號,但函數聲明不能直接調用的原因是:

  1. 小括號裏只能放表達式,不能放語句
  2. function關鍵字即可以當作語句,也可以當作表達式。但js規定function關鍵字出現在行首,一律解釋成語句

解決辦法:不讓 function 關鍵字出現在行首

function fn() {
    console.log(1);
}();    //報錯

const fn1 = function() {
    console.log('表達式執行');
}();    //執行函數

塊作用域

在 ES6 之前,js中也是有塊作用域概念的,但只限於個別具體的語法中:

  • with:用 with 從對象創建的出的作用域僅在 with 聲明中有效,而非外部作用域中。
  • try/catch:ES3 規定,try/catch 中的 catch 分句會創建一個塊級作用域,其中聲明的變量僅在 catch 內部有效。

在 ES6 中,引入了新的塊作用域

  • let:用來聲明變量,不允許聲明提升,也不允許重複聲明
  • const:用來聲明常量,不允許聲明提升,也不允許重複聲明

⚠️注意:提升是指聲明會被視爲存在於其所出現的作用域的整個範圍內。var允許變量聲明提升,但不允許賦值或其他運行邏輯提升。<font color=orange>函數聲明會被先提升,然後纔是變量</font>。

var scope = "global";
function scopeTest() {
    console.log(scope);
    var scope = "local" ; 
}
scopeTest(); //undefined

閉包

當函數可以記住並且訪問所在的詞法作用域時,並且函數是在當前詞法作用域之外執行,此時該函數和聲明該函數的詞法環境的組合。

不成功的代碼

直接看代碼吧,用語言來描述過於空洞。

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
      console.log(i);
  }, i * 1000);
}

這是一個高頻率會看到的題,我們期望的結果是:分別輸出數字1 - 5,每秒一個,每次一個。但實際上,會以每秒一次的頻率輸出五次6。

那代碼中到底有什麼缺陷導致它的行爲同語義所暗示的不一致呢?缺陷是我們試圖假設循環中每個迭代在運行時,都會爲自己"捕獲"一個 i 的副本。但是實際上,儘管這五個函數是在各個迭代中分別定義的,但是它們都<font color=orange>被封閉在一個共享的全局作用域中</font>,因此只有一個i。

如果想要返回的預期結果,可以通過以下方法。

立即執行函數表達式

在迭代內,使用 IIFE 會爲每個迭代都生成一個新的作用域,使得延遲函數的回調可以將新作用域封閉在每個迭代內部。

for (var i = 1; i <= 5; i++) {
  (function(j) {
      setTimeout(function timer() {
          console.log(j);
      }, i * 1000)
  })(i);
}

let 語法

let語法本質上是將一個塊轉換成一個可以被關閉的作用域,let聲明的變量在每次迭代都會聲明。

for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {

    console.log(i);
}, i * 1000)

}

<font color="blue">最後,如果覺得文章還不錯,請點個贊吧~👍</font>

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章