對於 JavaScript 來說,閉包是一個非常強大的特征。但對於剛開始接觸的初學者來說它又似乎是特別高深的。今天我們一起來揭開閉包的神秘面紗。閉包這一塊也有很多的文章介紹過了,今天我就淺談一下自己對閉包的的一些理解,希望能提供一點鄙陋的見解幫助到正在學習的朋友。該文章中能使用口語化的我將盡量使用口語化的敘述方式,希望能讓讀者更好理解,畢竟文章寫出來宗旨就是要讓人讀懂。文章難免有不足之處還希望幫忙指出。
在了解閉包之前,我們先來看看幾個准備知識。
變量的作用域
首先,什麼是作用域?域,區域。簡單來理解就是一個變量能被訪問的范圍(區域)。換言之就是這個變量能起作用的區域。按這個標准來劃分我們將變量分為 全局變量
和 局部變量
兩種
以定義的方式來區分有以下特點:
定義在函數內部的變量是局部變量,定義在函數外部的變量是全局變量。(這個並不只是 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
執行以上代碼結果如下圖所示
在代碼的最後一行拋出了 b 未定義的異常.也就是說我們在函數外部訪問不到在函數內部定義的局部變量。但是第六行代碼的正常輸出,可見在函數內部我們是可以訪問到在函數外部定義的全局變量 a
變量聲明提升
相信如果學過 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
小結一下
嵌套函數的作用域特點
搞清楚上面的小結部分我們縷一縷思路繼續探討另一個話題,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 中的作用域鏈。
我們先來總結上面提到的兩點
我們還是先上一段代碼
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 的變量。這樣的執行代碼結果以嵌套函數內的變量優先。可能這裡說成就近原則更容易記得住。這個就是閉包在實際應用中應該注意的一點。
關於閉包在開發中的使用,最多的體現應該還是在 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(){ function SayHi(str = '你好啊!'){ console.log(str); } return 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();
執行結果
今天對於閉包的看法暫時先寫到這了,秉承著學以致用的原則,下兩篇文章我將介紹 javascript 插件的幾種開發形式,以及實踐--開發一個原生的 Javascript 插件。
本文為作者原創,轉載請注明出處!