《你不知道的javascript》筆記_做用域與閉包

下一篇:《你不知道的javascript》筆記_thisjavascript

寫在前面

這一系列的筆記是在《javascript高級程序設計》讀書筆記系列的昇華版本,旨在將零碎未知的知識總結java

1、基本概念

1.1 編譯

在傳統編譯語言的流程中,程序中的一段源代碼在執行以前會經歷三個步驟,統稱爲編譯面試

clipboard.png

1. 分詞/詞法分析(Tokenizing/Lexing)編程

這個過程會將由字符組成的字符串分解成(對編程語言來講)有意義的代碼塊,這些代碼塊被稱爲 詞法單元(token)。例如,考慮程序 var a = 2;。這段程序一般會被分解成爲下面這些詞法單元: var、a、=、2 、;。空格是否會被看成詞法單元,取決於空格在這門語言中是否具備意義

2. 解析/語法分析(Parsing)segmentfault

這個過程是將詞法單元流(數組)轉換成一個由元素逐級嵌套所組成的表明了程序語法結構的樹。這個樹被稱爲 抽象語法樹(Abstract Syntax Tree,AST)

3. 代碼生成數組

將 AST 轉換爲可執行代碼的過程稱被稱爲代碼生成。拋開具體細節,簡單來講就是有某種方法能夠將 var a = 2;的 AST 轉化爲一組機器指令,用來建立一個叫做 a 的變量(包括分配內存等),並將一個值儲存在 a 中

相對於上面的流程,javascript在語法分析和代碼生成階段有特定的步驟來對運行性能進行優化,包括對冗餘元素進行優化等。閉包

1.2 組成

引擎:從頭至尾負責整個 JavaScript程序的編譯及執行過程

編譯器:負責語法分析及代碼生成等髒活累活異步

做用域:負責收集並維護由全部聲明的標識符(變量)組成的一系列查詢,並實施一套很是嚴格的規則,肯定當前執行的代碼對這些標識符的訪問權限編程語言

1.3 查詢&異常

RHS 查詢:當變量出如今賦值操做的右側時進行 RHS 查詢
LHS 查詢:當變量出如今賦值操做的左側時進行 LHS 查詢(賦值、傳參、函數執行)函數

RHS查詢異常:RHS 查詢在全部嵌套的做用域中遍尋不到所需的變量,引擎就會拋出 ReferenceError 異常
LHS查詢異常:非嚴格模式下,LHS 查詢失敗會在全局建立變量;在嚴格模式中 LHS 查詢失敗時,並不會建立並返回一個全局變量,引擎會拋出同 RHS 查詢失敗時相似的 ReferenceError 異常

1.4 思考

javascript引擎執行代碼var a = 2;的過程?

編譯階段:var a;,若是做用域內已存在變量 a,則忽略;若不存在,則在該做用域內聲明
執行階段:a = 2;,對 a 進行 LHS 引用,並對其賦值

2、做用域

負責收集並維護由全部聲明的標識符(變量)組成的一系列查詢,並實施一套很是嚴格的規則,肯定當前執行的代碼對這些標識符的訪問權限

通俗的說,做用域是維護變量並肯定訪問權限的一套規則

2.1 詞法做用域

詞法做用域就是定義在詞法階段的做用域。換句話說,詞法做用域是由你在寫代碼時將變量和塊做用域寫在哪裏來決定的,所以當詞法分析器處理代碼時會保持做用域不變(大部分狀況下是這樣的)

下面有個簡單的做用域嵌套的例子:

clipboard.png

【1】包含着整個全局做用域,其中只有一個標識符: foo。
【2】包含着 foo 所建立的做用域,其中有三個標識符: a、bar 和 b,可訪問全局做用域變量。
【3】包含着 bar 所建立的做用域,其中只有一個標識符: c,可訪問foo和全局做用域變量。

另外有兩個比較特殊的欺騙詞法機制:

  1. eval(..) 函數
  2. with 關鍵字
這兩個機制的反作用是引擎沒法在編譯時對做用域查找進行優化,由於引擎只能謹慎地認爲這樣的優化是無效的。使用這其中任何一個機制都將致使代碼運行變慢。 不要使用它們

2.2 做用域查找規則

書中對做用域鏈和做用域查找作了一個很是形象的比喻,以下圖

clipboard.png

這個建築表明程序中的嵌套做用域鏈。第一層樓表明當前的執行做用域,也就是你所處的位置。建築的頂層表明全局做用域。

LHS 和 RHS 引用都會在當前樓層進行查找,若是沒有找到,就會坐電梯前往上一層樓, 若是仍是沒有找到就繼續向上,以此類推。一旦抵達頂層(全局做用域),可能找到了你所需的變量,也可能沒找到,但不管如何查找過程都將中止

做用域查找會在找到第一個匹配的標識符時中止

2.2 塊級做用域

早期的javascript語句中塊級做用域就是函數塊,這是在讀本書以前我粗淺的認識。實際的塊級做用域遠不止如此

塊級做用域:
(1)函數做用域
早期盛行的當即執行函數(IIFE)就是爲了造成塊級做用域,不污染全局。經常使用的寫法有:

(function(形參){函數體})(實參)

(function(形參){函數體}(實參))

!function(形參){函數體}(實參)

(2) with關鍵字

(3) try/catch語句
Google 維護着一個名爲 Traceur 的項目,該項目正是用來將 ES6 代碼轉換成兼容 ES6 以前 的環境(大部分是 ES5,但不是所有),下面是用來兼容低版本建立塊級做用域的寫法:

{
    try {
        throw undefined;
    } catch (a) {
        a = 2;
        console.log( a );
    }
}

(4) let/const關鍵字

3、變量提高

在以前的兩篇文章中對變量提高(預解析)有比較充分的說明:
《javascript高級程序設計》筆記:變量對象與預解析
《javascript高級程序設計》筆記:內存與執行環境

4、閉包

4.1 什麼是閉包

本書中定義: 當函數能夠記住並訪問所在的詞法做用域時,就產生了閉包,即便函數是在當前詞法做用域以外執行。

MDN定義:閉包是函數和聲明該函數的詞法環境的組合

我的理解:當外部可以訪問到某個函數的私有變量時,就會產生閉包(不嚴謹,僅用於理解)

兩個經典的閉包例子:

function makeFunc() {
    var name = "Mozilla";
    function displayName() {
        alert(name);
    }
    return displayName;
}

var myFunc = makeFunc();
myFunc(); // 'Mozilla'

思考myFunc是執行makeFunc時建立的displayName函數實例的引用,爲何執行myFunc時會打印出makeFunc中私有變量name呢?

解釋閉包是由函數以及建立該函數的詞法環境組合而成。這個環境包含了這個閉包建立時所能訪問的全部局部變量

function makeAdder(x) {
  return function(y) {
    return x + y;
  };
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2));  // 7
console.log(add10(2)); // 12

分析:按照閉包能暫存變量的思路,執行makeAdder時,會把參數暫存在所return的函數中,當再次執行函數時,會把兩次的參數之和輸出

4.2 應用

閉包在js編程中隨處可見,書中有這樣一個結論:

在定時器、事件監聽器、 Ajax 請求、跨窗口通訊、Web Workers 或者任何其餘的異步(或者同步)任務中,只要使用了回調函數,實際上就是在使用閉包!

定時器閉包案例:

function wait(message) {
    setTimeout( function timer() {
        console.log( message );
    }, 1000 );
}
wait( "Hello, closure!" );

事件監聽閉包案例:

function setupBot(name, selector) {
    $(selector).click( function activator() {
        console.log( "Activating: " + name );
    });
}
setupBot( "Closure Bot 1", "#bot_1" );
setupBot( "Closure Bot 2", "#bot_2" );

上面的案例中,有個相同的特色:先定義函數,後執行函數時可以調用到函數中的私有變量或者實參。這即是閉包的特色吧

4.3 經典面試題

(1)下面的代碼輸出內容?

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

答案:5個6

(2)如何處理可以輸出1~5

// 閉包方式
for (var i=1; i<=5; i++) {
    (function(index) {
        setTimeout( function timer() {
            console.log( index );
        }, index*1000 );
    })(i)
}

// ES6 方式
for (let i=1; i<=5; i++) {
    setTimeout( function timer() {
        console.log( i );
    }, i*1000 );
}

下一篇:《你不知道的javascript》筆記_this

相關文章
相關標籤/搜索