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

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

做用域介紹

做用域是什麼?

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

何處?

做用域是能夠嵌套的,引擎從當前做用域開始查找,若是找不到,就會向上一級繼續查找,當抵達到最外層的全局做用域查找後,不管找到仍是沒找到,都會中止。閉包

<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 的屬性訪問。優化

舉🌰:code

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(...)

做用域的工做模型

主要有兩種:cdn

  • 詞法做用域:由你在書寫代碼時將變量和塊做用域寫在哪裏來決定的。有時候可能會有在代碼運行時「修改」詞法做用域的需求,能夠經過如下機制:對象

    • 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>

相關文章
相關標籤/搜索