淺析 JavaScript 中的閉包(Closures)

1、前言

對於 JavaScript 來講,閉包是一個很是強大的特徵。但對於剛開始接觸的初學者來講它又彷佛是特別高深的。今天咱們一塊兒來揭開閉包的神祕面紗。閉包這一塊也有不少的文章介紹過了,今天我就淺談一下本身對閉包的的一些理解,但願能提供一點鄙陋的看法幫助到正在學習的朋友。該文章中能使用口語化的我將盡可能使用口語化的敘述方式,但願能讓讀者更好理解,畢竟文章寫出來宗旨就是要讓人讀懂。文章不免有不足之處還但願幫忙指出。javascript

2、Javascript 的做用域鏈

在瞭解閉包以前,咱們先來看看幾個準備知識。html

  1. 變量的做用域java

    首先,什麼是做用域?域,區域。簡單來理解就是一個變量能被訪問的範圍(區域)。換言之就是這個變量能起做用的區域。按這個標準來劃分咱們將變量分爲 全局變量局部變量 兩種web

    以定義的方式來區分有如下特色:session

    定義在函數內部的變量是局部變量,定義在函數外部的變量是全局變量。(這個並不僅是 Javascript 語言的特色)局部變量在函數內部能被訪問,在函數外部不能被直接訪問,因此局部變量就是從定義它的地方開始到函數結束的位置結束。固然這裏有個細節--變量聲明提高。等下咱們用一小段代碼提一下變量聲明提高是什麼。咱們先來看看局部變量和全局變量的代碼閉包

    var a = 0;
    
    function testFunc(){
        var b = 1;
        console.log('-------------函數內輸出-------------');
        console.log(a);//0
        console.log(b);//1
    }
    
    //調用函數
    testFunc();
    
    console.log('-------------函數外輸出-------------');
    console.log(a);//0
    console.log(b);//Uncaught ReferenceError: b is not defined

     

    執行以上代碼結果以下圖所示ide

    代碼執行結果

    在代碼的最後一行拋出了 b 未定義的異常.也就是說咱們在函數外部訪問不到在函數內部定義的局部變量。可是第六行代碼的正常輸出,可見在函數內部咱們是能夠訪問到在函數外部定義的全局變量 a函數

    變量聲明提高post

    相信若是學過 C 語言的話,應該會很熟悉一句話 "先聲明後使用"。就是說一個變量或者函數在使用它以前必須是要先找獲得這個變量或函數的聲明的。例如:學習

    //C 語言正確寫法
    int a = 0;
    printf(a);
    
    //錯誤寫法,下面代碼沒辦法經過標準編譯(直接報異常)
    printf(a);
    int a = 0;

     

    咱們再來看看 Javascript 代碼

    var a = 0;
    console.log(a);//輸出結果 0

     

    上面這種普通寫法咱們不探討,重點看下面的這段代碼

    console.log(a);//輸出結果 undefined
    var a = "hello";
    console.log(a);//輸出結果 hello

    運行結果以下

    上面這個例子就剛好說明了變量聲明提高的特色,咱們在沒有聲明變量 a 以前就直接訪問變量a 輸出結果爲 undefined 而並非直接報異常。因此最直觀的感受是變量的聲明被提高到使用以前了。實質上代碼以下:

    var a;//聲明被提高到這裏
    console.log(a);//輸出結果 undefined
    a = "hello";
    console.log(a);//輸出結果 hello

     

    小結一下

    • 函數內部定義的變量是局部變量,函數外部定義的變量是全局變量。
    • 局部變量不能被外界直接訪問,全局變量能夠在函數內被訪問。
    • 變量聲明提高
  2. 嵌套函數的做用域特色

    搞清楚上面的小結部分咱們縷一縷思路繼續探討另外一個話題,javascript 中的嵌套函數,咱們先上一段代碼:

    function A(param){
        var vara = 1;   
        function B(){
            var varb = 2;
            console.log("----Function B----------")
            console.log(vara);//函數B中訪問A函數中定義的變量
            console.log(param);//A函數中傳進來的變量
            console.log(varb);//訪問自身函數內定義的變量
        }
        B();
        console.log("----Function A----------")
        console.log(vara);//訪問自身函數內定義的變量
        console.log(param);//A函數中傳進來的變量
        console.log(varb);//訪問B函數中定義的變量--異常
    }
    A("hello");

    運行結果以下:

    函數嵌套

    因而可知嵌套函數(B)能夠繼承容器函數(A)的參數和變量,可是嵌套函數(B)中的變量對於他的容器函數來講倒是B私有的,也就是說 A 沒法訪問 B 中定義的變量。換句話說,B 函數造成了一個相對獨立的環境(空間)使得它自身的變量只能由它本身來訪問,可是 A 函數裏的變量 B 也能夠訪問,這裏嵌套函數 B 就造成了一個閉包。有一句話很適合 B 來講 「你的就是個人,個人仍是個人」

    從語法上看是函數 A 包含着函數 B,可是從做用域上來看是函數 B 的做用域包含着函數 A 的做用域,關係以下圖所示:

    函數嵌套

    假設:函數 B 下面又包含了函數 C。此時函數 C 爲函數 B 的嵌套函數,函數 B 爲函數 C 的容器函數。對於C來講也具備剛剛講過的 「你的就是個人,個人仍是個人」 的特色。以此類推層層嵌套的話就造成了一條鏈條, 做用域按此規律也造成了 Javascript 中的做用域鏈。

    函數嵌套

3、閉包的特色

咱們先來總結上面提到的兩點

  • 嵌套在容器函數(A)內部的嵌套函數(B)只能在容器函數(A)內被訪問
  • 嵌套函數(B)繼承了容器函數(A)的變量,可是 B 函數中的變量只有它本身能訪問,也就是嵌套函數(B)的做用域包含容器函數(A)的做用域。
閉包之保存變量

咱們仍是先上一段代碼

function A(a){
    function B(b){
        return a + b;
    }
    return B;
}
var C = A(1);
var result = C(2);
console.log(result);//輸出結果 3 

函數 B 造成了一個閉包,A 函數調用以後返回函數 B 的引用。執行 C 以後發現結果等於3,這也就說明了咱們調用 A 的時候 傳進去的參數 1 沒有被銷燬,而是被保存起來了,這就是閉包保存變量的特色。

有保存就有銷燬那咱們被閉包保存的變量在何時銷燬?答案是當 B 沒有再被引用的時候,就會被銷燬.

閉包的注意點--命名衝突

咱們仍是先上一段代碼

function A(){
    var num = 6;//外部的名爲num 的變量
    function B(num){
        return num;//當作參數傳進來的num 變量,命名衝突發生在這
    }
    return B;
}
var result = A()(10);
console.log(result);//輸出結果10

上述代碼的執行結果

閉包中的命名衝突

經過上面的代碼咱們能看到有一個容器函數內的名爲 num 的變量以及一個嵌套函數內一樣名爲 num 的變量。這樣的執行代碼結果以嵌套函數內的變量優先。可能這裏說成就近原則更容易記得住。這個就是閉包在實際應用中應該注意的一點。

4、閉包在開發中的應用。

關於閉包在開發中的使用,最多的體現應該仍是在 Javascript 插件的開發上面。使用閉包能夠避免變量污染。也就是說你在閉包中使用的變量名稱不會影響到其餘地方一樣名稱,換個角度來說,我將我嵌套函數內部的變量給保護起來了,外部沒辦法隨便修改我內部定義的變了。也就是雖然名字同樣可是你是你我是我。代碼體現以下:

function A(){
    function B(num){
        var c = 10;//內部變量 c
        return num + c;
    }
    return B;
}

var c = 20;//外部變量c
var result = A()(c);
console.log(c);//20 
console.log(result)//30 

以上特色應用在插件開發中就能夠很好的保護了插件自己,避免了外界的串改,保證了插件的穩定。

簡單的插件

初步代碼

//編寫插件代碼
var plugin = (function(){
   var _sayhi = function(str = '你好啊!'){
        console.log(str);
    }
   return {
        SayHi : _sayhi
    }
})(); //使用插件 plugin('hello'); plugin();

插件初步

上面代碼閉包部分我就不在累述了,咱們來看看新出現的一種語法--自調用匿名函數:

(function{
    //code
})();

實際做用是建立了一個匿名函數,並在建立後當即執行一次。做用等價於下面的代碼,惟一的區別就是下面的函數不是匿名的。

//建立
var func = function(){
    //code
}   
//調用
func();

固然,咱們編寫插件不可能只提供一個API給外部使用,如何返回多個API,咱們這裏使用字面量形式返回。改進以後的代碼以下

//編寫插件代碼
var plugin = (function(){
    var _sayhi = function(str = '你好啊!'){
        console.log(str);
    }
    var _sayhello = function(){
        console.log("這個API能作很牛逼的事情");
    }
    return {
        SayHi : _sayhi,
        SayHello : _sayhello
    }
})();

//經過插件提供的API使用插件
plugin.SayHi('hello');
plugin.SayHello();

執行結果

閉包中的命名衝突

5、後語

今天對於閉包的見解暫時先寫到這了,秉承着學以至用的原則,下兩篇文章我將介紹 javascript 插件的幾種開發形式,以及實踐--開發一個原生的 Javascript 插件。

6、補充

技術因交流而進步,很感激各位提供意見的博友。寫文章的目的就是爲了給正在學習階段的朋友一些參考,固然我最怕給的是誤導。針對博友指出的東西我在這裏補充一下,但願不會打亂大家以前對閉包的理解。因此請將上述知識點理解爲「從實踐角度理解的閉包」,其實關於 JS 閉包的概念我以爲能夠從廣義和狹義(實踐)上來理解,既然有朋友提到了咱們就繼續延伸一下。先來看看《JavaScript高級程序設計》第三版 中對閉包的定義「閉包是定義在一個外部函數內部,而且可以訪問(存取)外部函數中自由變量的函數」,按這樣的定義那麼久必須是在嵌套函數的前提下才能造成閉包。咱們上文所講的例子都是基於這一種。

咱們再來看下另外一個閉包的定義

「閉包,是指語法域位於某個特定的區域,具備持續參照(讀寫)位於該區域內自身範圍以外的執行域上的非持久型變量值能力的段落」

感受挺抽象,不要緊。咱們來看下下面代碼

var a = 10;
function A(){
    console.log(a);//10(讀的能力)
    a = 20;//(寫的能力)
    console.log(a);//20 
}
A();

經過代碼咱們重新來看這一段定義閉包的文字

「閉包,是指語法域位於某個特定的區域(A 函數),具備持續參照(讀寫)位於該區域內自身範圍以外的執行域上的非持久型變量(上述的變量 a)值能力的段落」

那麼這樣子定義的話,Javascript 中的全部函數都有這樣的能力。因此也就有了這樣的說法:
Javascript 中的函數都是一個閉包。

關於閉包,本文先更新到這裏。其實閉包涉及的基礎知識比較多,有些許基礎(好比執行上下文,變量對象以及funarg 等)知識點本文尚未說起,後續文章會有所補充。

限於筆者技術,文章觀點不免有不當之處,但願發現問題的朋友幫忙指正,筆者將會及時更新。也請轉載的朋友註明文章出處並附上原文連接,以便讀者能及時獲取到文章更新後的內容,以避免誤導讀者。筆者力求避免寫些晦澀難懂的文章(雖然也有人說這樣顯得高逼格,專業),儘可能使用簡單的用詞和例子來幫助理解。若是表達上有好的建議的話也但願朋友們在評論處指出。

本文爲做者原創,轉載請註明出處! Cboyce

相關文章
相關標籤/搜索