JavaScript之做用域和閉包

1、做用域

  1. 做用域共有兩種主要的工做模型:第一種是最爲廣泛的,被大多數編程語言所採用的詞法做用域,另一種叫做動態做用域;
  2. JavaScript所採用的做用域模式是詞法做用域。

1.詞法做用域

  1. 詞法做用域意味着做用域是由書寫代碼時函數聲明的位置來決定的。編譯的詞法分析階段基本可以知道所有標識符在哪裏以及是如何聲明的,從而可以預測在執行過程當中如何對它們進行查找。
  2. JavaScript 中有兩個機制能夠「欺騙」詞法做用域:編程

    • eval(..):能夠對一段包含一個或多個聲明的「代碼」字符串進行演算,並藉此來修改已經存在的詞法做用域(在運行時) ;
    • with:經過將一個對象的引用看成做用域來處理,將對象的屬性看成做用域中的標識符來處理,從而建立了一個新的詞法做用域(一樣是在運行時) 。
    • 這兩個機制的反作用是引擎沒法在編譯時對做用域查找進行優化,由於引擎只能謹慎地認爲這樣的優化是無效的。使用這其中任何一個機制都將致使代碼運行變慢。

2.函數做用域和塊級做用域

  1. 函數做用域: 函數是 JavaScript 中最多見的做用域單元。本質上,聲明在一個函數內部的變量或函數會在所處的做用域中「隱藏」起來,即函數內定於的函數和變量爲該函數私有;
  2. 塊級做用域:閉包

    • 塊做用域指的是變量和函數不只能夠屬於所處的做用域,也能夠屬於某個代碼塊(一般指 { .. } 內部)
    • ES6前在JavaScript中並不存在塊級做用域( 例外:try/catch 結構在 catch 分句中具備塊做用域);
    • 在 ES6 中引入了 let 關鍵字( var 關鍵字的表親) ,用來在任意代碼塊中聲明變量。 if(..) { let a = 2; } 會聲明一個劫持了 if 的 { .. } 塊的變量,而且將變量添加到這個塊中(另外常量定義const也具備塊級做用域)。

3.函數和變量的提高

(1)、提高編程語言

  1. 函數做用域和塊做用域的行爲是同樣的,即,某個做用域內的變量,都將附屬於這個做用域。
  2. 引擎會在解釋 JavaScript 代碼以前首先對其進行編譯。編譯階段中的一部分工做就是找到全部的聲明,並用合適的做用域將它們關聯起來;
  3. 所以包括變量和函數在內的全部聲明都會在任何代碼被執行前首先被處理;
  4. 當看到 var a = 2; 時,可能會認爲這是一個聲明。但 JavaScript 實際上會將其當作兩個聲明: var a; 和 a = 2; 。第一個定義聲明是在編譯階段進行的。第二個賦值聲明會被留在原地等待執行階段。函數

    • 這個過程就好像變量和函數聲明從它們在代碼中出現的位置被「移動」到了最上面。這個過程就叫做提高
  5. 每一個做用域都會進行提高操做;

(2)、函數優先性能

  1. 函數聲明和變量聲明都會被提高。可是函數會首先被提高,而後纔是變量。
foo(); // 1
var foo;
function foo() {
    console.log( 1 );
}
foo = function() {
    console.log( 2 );
};
  • 會輸出 1 而不是 2 !這個代碼片斷會被引擎理解爲以下形式:
function foo() {
    console.log( 1 );
}
foo(); // 1
foo = function() {
    console.log( 2 );
};
  • var foo 儘管出如今 function foo()... 的聲明以前,但它是重複的聲明(所以被忽略了) ,由於函數聲明會被提高到普通變量以前。
  • 儘管重複的 var 聲明會被忽略掉,但出如今後面的函數聲明仍是能夠覆蓋前面的。

2、做用域閉包

(1)、理解閉包

  • 當函數能夠記住並訪問所在的詞法做用域時,就產生了閉包,即便函數是在當前詞法做用域以外執行。
  1. 在Javascript語言中,只有函數內部的子函數才能讀取局部變量,所以能夠把閉包簡單理解成"定義在一個函數內部的函數"。
  2. 在本質上,閉包就是將函數內部和函數外部鏈接起來的一座橋樑

(2)、閉包的用途

  1. 能夠讀取函數內部的變量;
  2. 讓變量的值始終保持在內存中。

(3)、閉包的產生實例

  1. 能夠讀取函數內部的變量
function foo() {
var a = 2;
function bar() {
    console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 —— 這就是閉包的效果。
  • 在 foo() 執行後,一般會期待 foo() 的整個內部做用域都被銷燬,由於咱們知道引擎有垃圾回收器用來釋放再也不使用的內存空間;
  • 閉包的「神奇」之處正是能夠阻止這件事情的發生。事實上內部做用域依然存在,所以沒有被回收,由於 bar() 自己在使用;
  • 拜 bar() 所聲明的位置所賜,它擁有涵蓋 foo() 內部做用域的閉包,使得該做用域可以一直存活,以供 bar() 在以後任什麼時候間進行引用。
  • bar() 依然持有對該做用域的引用,而這個引用就叫做閉包。
  1. 循環和閉包:
for (var i=1; i<=5; i++) {
    setTimeout( function timer() {
        console.log( i );
    }, i*1000 );
}
  • 正常狀況下,咱們對這段代碼行爲的預期是分別輸出數字 1~5,每秒一次,每次一個。但實際上,這段代碼在運行時會以每秒一次的頻率輸出五次 6:優化

    • 延遲函數的回調會在循環結束時才執行。事實上,當定時器運行時即便每一個迭代中執行的是 setTimeout(.., 0) ,全部的回調函數依然是在循環結束後纔會被執行,所以會每次輸出一個 6 出來。
    • 實際狀況是儘管循環中的五個函數是在各個迭代中分別定義的,可是它們都被封閉在一個共享的全局做用域中,所以實際上只有一個 i,即全部函數共享一個 i 的引用 。
  • 解決方案:使用 IIFE在每次迭代中將本次迭代的i傳入建立的做用域並封閉起來;
for (var i=1; i<=5; i++) {
    (function(j) {
        setTimeout( function timer() {
            console.log( j );
        }, j*1000 );
    })( i );
}
  • 在迭代內使用 IIFE 會爲每一個迭代都生成一個新的做用域,使得延遲函數的回調能夠將新的做用域封閉在每一個迭代內部,每一個迭代中都會含有一個具備正確值的變量供咱們訪問。

(4)、使用閉包的注意點

  1. 因爲閉包會使得函數中的變量都被保存在內存中,內存消耗很大,因此不能濫用閉包,不然會形成網頁的性能問題,在IE中可能致使內存泄露。code

    • 解決方案:在退出函數以前,將不使用的局部變量所有刪除。
  2. 閉包會在父函數外部,改變父函數內部變量的值。因此,若是把父函數看成對象(object)使用,把閉包看成它的公用方法(Public Method),把內部變量看成它的私有屬性(private value),這時必定要當心,不要隨便改變父函數內部變量的值。
相關文章
相關標籤/搜索