【共讀】《你不知道的JavaScript上》做用域與閉包

本文會用導圖梳理本書的脈絡,因爲是導讀,正文部分只會列舉重點內容,非重點內容會簡單介紹,歡迎討論與閱讀原文。此外本文適合未讀過此書的同窗參考是否須要閱讀,另外讀過此書的同窗,能夠嘗試回答文初的問題及順着導圖回憶本書內容,若是很是流暢那麼相信您對書中的知識的理解是過關的。面試

上一篇咱們講了本書第一章中的做用域,做用域鏈,簡單介紹了引擎,編譯器,做用域是怎麼合做進行編譯的。本篇咱們將會介紹本書第一部分的 2 ~ 6 章。緩存

問題

  1. 說說你對閉包的理解。(面試)
  2. 說說你對詞法做用域的理解,咱們如何欺騙詞法做用域?
  3. 函數做用域的做用?如何避免函數做用域污染全局變量?
  4. JavaScript 除了函數做用域還存在哪些塊做用域?
  5. 變量提高的機制是什麼?函數聲明和變量什麼那個優先被提高?

第一部分做用域與閉包

說說你對閉包的理解? (我的理解,求拍磚,不太瞭解閉包的同窗能夠暫時跳過)性能優化

閉包是一個綁定了執行環境的函數,閉包和普通函數的區別是攜帶了執行的環境。bash

閉包由環境和表達式兩部分組成。閉包

  • 環境部分
    • 環境:函數的詞法做用域(執行上下文的一部分)。
    • 標識符列表:函數中用到的未聲明的變量。
  • 表達式部分:函數體。

它被普遍應用,好比在回調函數中定時器,事件監聽器,Ajax請求,跨窗口通訊……又好比咱們瞭解的循環,還有模塊。閉包能夠賦予了咱們訪問與操做上級做用域的能力爲開發提供了極大的便利。異步

若是要說閉包的缺點可能就是維護困難,閉包能夠緩存上級做用域的變量,若是閉包又是異步執行必定要搞清楚上級做用域都發生了什麼,對代碼的運行機制和邏輯要有所瞭解。函數

1、詞法做用域?

  1. 做用域分紅詞法做用域動態做用域post

    詞法做用域是一套關於引擎如何尋找變量以及在何處找到變量的規則,詞法做用域最重要的特徵就是它定義的過程發生在代碼的書寫階段(假設沒有使用eval() or with)。性能

    動態做用域讓做用域在運行時動態肯定。優化

    詞法階段就是你說了一句話,而後編譯器斷句的過程。像不一樣的人理解一句話不一樣斷句也就不一樣同樣,不一樣的編譯器分詞也會有所不一樣。

  2. 詞法做用域中有環境記錄器外部環境的引用,想要深刻理解,咱們須要瞭解執行上下文和執行棧(書上沒有提到)

  3. 查找過程

    function foo(a) {
        var b = "Java"
        function bar(c) {
            var b = "你好"
            console.log(a + b + c)
        }
        bar("Script")
    }
    foo("你不知道的")
    複製代碼

    「遮蔽效應」 做用域查找會在找到第一個匹配的標識符時中止,咱們在多層嵌套的做用域中能夠定義同名的標識符,就可能由於遮蔽效應而找不到外部的標識符。

    「全局對象」 全局變量會自動成爲全局對象的屬性,所以能夠不直接經過全局對象的詞法名稱查找。

    window.a  
    複製代碼

    經過這種方式咱們能夠訪問被遮蔽的全局變量,可是非全局的變量若是被遮蔽了,不管如何也訪問不了。

  4. 改變詞法做用域的方法?

    詞法做用域徹底由寫代碼期間函數聲明的位置來定義,怎麼能夠在運行時修改呢?

    改變詞法做用域的方法有兩種,evalwith

    JavaScript 引擎會在編譯階段進行數項的性能優化,其中有些優化依賴於詞法的靜態分析,必須預先肯定全部變量和函數的定義位置,一但在引擎中發現了上述的兩種方法,那麼這些優化都是無效的。

    因此這兩種方式不建議使用,想看的同窗能夠查資料或者翻閱本書。

2、 函數做用域與塊做用域

  1. 函數做用域是什麼?

    每聲明一個函數,就會爲自身建立一個做用域,在這個做用域中,屬於函數的所有變量均可以在整個函數的範圍內(包括嵌套在內的做用域)使用及複用。

  2. 函數做用域的做用?

    隱藏,在當前函數做用域內聲明的變量或函數都會綁定在當前的函數做用域中,這樣在全局做用域中就訪問不到它們了,咱們能夠叫這個函數叫包裝函數

    這符合軟件設計中應該最小限度得暴露必要內容,將其餘內容都「隱藏」起來的原則,好比模塊或對象API設計。

    規避衝突,避免同名標識符之間的衝突。

  3. 當即執行函數表達式的做用?

    (function foo() {
        var a = "zhengyang";
        console.log(a);
    })();
    複製代碼

    因爲函數被包含在一對()中就成爲了一個表達式,經過在末尾加一個()能夠當即執行這個函數。它能夠避免 foo 函數名稱污染全局做用域,而且也不須要經過函數名 foo()來調用。

    社區稱它爲LIFE,而且咱們每每使用匿名函數,即省略掉 foo,由於函數表達是能夠匿名的。

  4. let的機制?

    let關鍵字能夠將變量綁定到所在的任意做用域中(一般是在{}中),能夠說let爲其聲明的變量隱式得劫持了所在的塊做用域。(表面上 JavaScript不具備塊做用域的相關功能,但其實 with ,try/catch 是能夠建立塊做用域的)

3、 提高

  1. 包括變量和函數在內的全部聲明都會在任何代碼被執行前首先被處理。

  2. 函數聲明優先於變量聲明。

    咱們把 var a = 2 看作一個聲明,事實上這個表達式能夠拆成 var a;a = 2 ,第一部分是變量聲明發生在編譯階段,第二部分的賦值會被留在原地等待執行階段。

    console.log(zy) // ƒ zy() {}
    
    function zy() {
    }
    console.log(zy); // ƒ zy() {}
    var zy = 2
    
    console.log(zy); // 2
    複製代碼

    在上面的代碼中在編譯階段會進行聲明的提高,首先提高函數聲明 function zy(){} 而後再提高 var zy ,而後逐條執行,因此一個 console.log(zy) 輸出了函數 , 而後 第二個console.log(zy) 輸出了函數,最後咱們用留在原地的 zy = 2 把 2 賦給了 zy 因此最後一個 console.log(zy) 輸出了 2。

4、 循環與閉包

for (var i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i)
  }, i * 1000);
}
複製代碼

不難理解以上的代碼輸出是 5 個 5,由於 setTimeout 是異步的因此會在for循環結束後纔會執行。可是咱們想要獲得的倒是每循環一次輸出一次,結果是 1 , 2 , 3 , 4 , 5。

使用閉包的方法解決問題。

for (var i = 1; i <= 5; i++) {
  (function () {
    var j = i;
    setTimeout(() =>{
      console.log(j)
    }, j * 1000)
  })();
}
複製代碼

在上面的代碼中咱們的環境是全局做用域,函數體是一個當即執行函數,每次的循環 i 的值都由於閉包被保留了下來傳遞給 j 而後經過 setTimeout 輸出一次值。

for (var i = 1; i <= 5; i++) {
 (function (j) {
   setTimeout(() =>{
     console.log(j)
   }, j * 1000)
 })(i);
}
複製代碼

咱們也能夠用 let

for (let i = 1; i <= 5; i++){
    setTimeout(() =>{
        console.log(i)
    }, i*1000)
}
複製代碼

後言

閉包這塊我讀到這裏已然對它有了一個全新的認識,可是仍然存在很多問題,好比具體編譯的過程;JavaScript引擎如何作優化;同步異步的理解還要有待挖掘,對於 let 的實現也不是很瞭解。

相關文章
相關標籤/搜索