JavaScript深刻之做用域和閉包

js做用域

做用域定義

《JavaScript權威指南》中講到:一個變量的做用域是程序源代碼中定義這個變量的區域。數據結構

而實際上來講,做用域就是一套存儲變量的規則,用於肯定在何處以及如何查找變量。閉包

編譯原理

儘管一般將JavaScript稱做爲 動態解釋型 語言,但實際上JavaScript是有編譯過程的。模塊化

  • 詞法分析(lexical analysics):將代碼分解成詞法單元
  • 語法分析(parsing):將代碼整理成 抽象語法樹(AST)
  • 代碼生成:將AST轉化成可執行代碼

理解做用域

做用域相關的核心組件

  • 引擎:負責整個JavaScript程序的編譯和執行過程
  • 編譯器:負責整個編譯過程
  • 做用域
    • 負責收集變量聲明
    • 提供變量聲明查詢
    • 控制代碼對變量的訪問權限

當看到 var a = 2,這段程序時,咱們來看看編譯器和引擎都作了什麼?函數

  1. 首先編譯器會在當前做用域中查詢是否已經有一個名爲a的變零存在於同一個做用域的集合中,若是有,則忽略該聲明,繼續進行編譯,若是沒有,則會要求做用域在當前的做用域集合中聲明一個變量a。ui

  2. 編譯器將 var a = 2 這個代碼片斷編譯成用於執行的機器指令spa

  3. 引擎在運行時會首先在當前做用域中查找是否存在變量a,若是不存在則繼續像上一級查找,若是存在,則引擎會使用這個變量設計

  4. 若是引擎最終找到了這個變量a,就會將2賦值給它,若是沒有找到則會拋出一個異常。3d

詞法做用域

簡單地說,詞法做用域就是定義在詞法階段的做用域,也就是說,詞法做用域是你在書寫代碼時就已經決定了的。code

看一下如下代碼cdn

function foo(a) { 
 var b = a * 2;
 function bar(c) {
  console.log( a, b, c );
 }
 bar( b * 3 ); 
}
foo( 2 ); // 2, 4, 12
複製代碼

在這個例子中有三個嵌套的做用域:

① 包含着整個全局做用域,只有一個標識符:foo ② 包含着 foo 函數所建立的做用域,有三個標識符:a、b、bar ③ 包含着 bar 函數所建立的做用域,有一個標識符:c

做用域氣泡由其對應的做用域塊代碼寫在哪裏決定,它們是逐級包含的。

做用域的分類

  • 全局做用域
  • 函數做用域
  • 塊級做用域

變量提高

先看一段代碼:

var a;
a = 2; 
console.log( a );
複製代碼

執行結果是 2。

實際上JavaScript代碼在執行時候並非一行一行的執行的,引擎在執行代碼以前會首先對其進行編譯,包括變量和函數在內的全部聲明都會在任何代碼被執行前被‘移動’到最上面,這就叫作變量提高。

再來看一個例子:

foo();
function foo() {
  console.log( a ); // undefined 
  var a = 2;
}
複製代碼

值得注意的是,每一個做用域都會進行提高操做。只有聲明自己會被提高,而賦值或其餘運行邏輯仍會留在原地。

所以上面那段代碼能夠被理解爲下面的形式:

function foo() { 
  var a;
  console.log( a ); // undefined
  a = 2;
}
foo();
複製代碼

具體的提高規則以下:

  • 函數聲明會被優先提高
  • 重複的var聲明會被忽略掉
  • 後聲明的函數會覆蓋掉先聲明的函數
  • 函數的聲明會被提高到當前做用域頂部,不收代碼邏輯控制

做用域鏈

當一個塊或函數嵌套在另外一個塊或函數中時,就發生了做用域的嵌套。所以引擎在查找變量時,會先從當前的執行做用域開始查找,若是找不到,就向上一級繼續查找,當到達最外層的全局做用域時,不管找到仍是沒找到,查找過程就會中止。像這樣由多個執行上下文的變量對象構成的鏈式結構就叫作做用域鏈。

閉包

閉包的定義

閉包的定義有不少種,可是通常能夠分爲兩類

  • 一種說法是閉包是符合必定條件的函數。好比《JavaScript⾼級程序設計》是這樣定義的:閉包是指有權訪問另外一個函數做用域中的變量的函數。

  • 另外一種說法是閉包是由函數以及和它相關的引用環境組合而成的實體。好比MDN中是這樣定義的:閉包是函數和聲明該函數的詞法環境的組合。

這兩種說法在某種意義上實際上是對立的,一個認爲閉包是函數,另外一個認爲閉包是函數和引用環境組成的總體。但其實第二種的表達看起來更確切點,MDN稍早以前給出的閉包定義是: 閉包是那些可以訪問自由變量(自由變量是指在函數中使用的,但既不是函數參數也不是函數的局部變量的變量。)的函數,這種說法是比較結交接近於第一種說法的,可是後來的MDN定義卻改爲了上面的第二種說法。

函數只是一些可執行的代碼,而閉包的本質來源於兩點:

  • 詞法做用域:函數外部的代碼沒法訪問函數體內部的變量,而函數體內部的代碼能夠訪問函數外部的變量。
  • 函數當作值傳遞:即所謂的函數是一等公民(First-class value)。函數能夠做爲做爲另外一個函數的返回值和參數。原本執行過程和詞法做用域是封閉的,這種返回的函數就比如是一個通道,這個通道能夠訪問這個函數詞法做用域中的變量,即函數所須要的數據結構保存了下來,這‘通道’說的就是內部函數對詞法做用域的引用。

即便函數已經執行完畢,在執行期間建立的變量也不會被銷燬,所以每運行一次函數就會在內存中留下一組變量。(js固然會有垃圾回收機制,不過若是它發現你正在使用閉包,則不會清理可能會用到的變量)

因此咱們概括一下,就是關於一個函數要成爲一個閉包到底須要滿意幾個條件:

  • 函數嵌套,即基於詞法做用域的查找規則
  • 將內部函數做爲值返回
  • 在所在做用域外被調用

下面代碼就是一個典型的閉包

function foo(){
    var a = 2;
    function bar(){
        console.log(a);//2
    }
    return bar;
}
var baz = foo();
baz(); 
baz = null;
複製代碼

因爲閉包占用內存空間,因此要謹慎使用閉包。儘可能在使用完閉包後,及時解除引用,以便更早釋放內存。

閉包的做用

  • 模塊化
  • 保存私有變量
相關文章
相關標籤/搜索