聊一下JS中的做用域scope和閉包closurejavascript
scope和closure是javascript中兩個很是關鍵的概念,前者JS用多了還比較好理解,closure就不同了。我就被這個概念困擾了好久,不管看別人如何解釋,就是不通。不過理越辯越明,代碼寫的多了,小程序測試的多了,再回過頭看看別人寫的帖子,也就漸漸明白了閉包的含義了。咱不是啥大牛,因此不搞的那麼專業了,惟一的想法就是試圖讓你明白什麼是做用域,什麼是閉包。若是看了這個帖子你還不明白,那麼多寫個把月代碼回過頭再看,相信你必定會有收穫;若是看這個帖子讓你收穫到了一些東西,告訴我,仍是很是開森的。廢話很少說,here we go!java
一、function小程序
在開始以前呢,先澄清一點(廢話咋這麼多捏),函數在JavaScript中是一等公民。什麼,你聽了不少遍了?!!!。那這裏我須要你明白的是,函數在JavaScript中不只能夠調用來調用去,它自己也能夠當作值傳遞來傳遞去的。閉包
二、scope及變量查詢異步
做用域,也就是咱們常說的詞法做用域,說簡單點就是你的程序存放變量、變量值和函數的地方。函數
塊級做用域測試
若是你接觸過塊級做用域,那麼你應該很是熟悉塊級做用域。簡單說來就是,花括號{}括起來的代碼共享一塊做用域,裏面的變量都對內或者內部級聯的塊級做用域可見。spa
基於函數的做用域code
在JavaScript中,做用域是基於函數來界定的。也就是說屬於一個函數內部的代碼,函數內部以及內部嵌套的代碼均可以訪問函數的變量。以下:blog
上面定義了一個函數foo,裏面嵌套了函數bar。圖中三個不一樣的顏色,對應三個不一樣的做用域。①對應着全局scope,這裏只有foo②是foo界定的做用域,包含、b、bar③是bar界定的做用域,這裏只有c這個變量。在查詢變量並做操做的時候,變量是從當前向外查詢的。就上圖來講,就是③用到了a會依次查詢③、②、①。因爲在②裏查到了a,所以不會繼續查①了。
這裏順便講講常見的兩種error,ReferenceError和TypeError。如上圖,若是在bar裏使用了d,那麼通過查詢③、②、①都沒查到,那麼就會報一個ReferenceError;若是bar裏使用了b,可是沒有正確引用,如b.abc(),這會致使TypeError。
嚴格的說,在JavaScript也存在塊級做用域。以下面幾種狀況:
①with
1 var obj = {a: 2, b: 2, c: 2}; 2 with (obj) { //均做用於obj上 3 a = 5; 4 b = 5; 5 c = 5; 6 }
②let
let是ES6新增的定義變量的方法,其定義的變量僅存在於最近的{}以內。以下:
var foo = true; if (foo) { let bar = foo * 2; bar = something( bar ); console.log( bar ); } console.log( bar ); // ReferenceError
③const
與let同樣,惟一不一樣的是const定義的變量值不能修改。以下:
1 var foo = true; 2 if (foo) { 3 var a = 2; 4 const b = 3; //僅存在於if的{}內 5 a = 3; 6 b = 4; // 出錯,值不能修改 7 } 8 console.log( a ); // 3 9 console.log( b ); // ReferenceError!
三、scope的如何肯定
不管函數是在哪裏調用,也不管函數是如何調用的,其肯定的詞法做用域永遠都是在函數被聲明的時候肯定下來的。理解這一點很是重要。
四、變量名提高
這也是個很是重要的概念。理解這個概念前,須要瞭解的是,JS代碼的執行過程分爲編譯過程和執行。舉例以下:
1 var a = 2;
以上代碼其實會分爲兩個過程,一個是 var a; 一個是 a = 2; 其中var a;是在編譯過程當中執行的,a =2是在執行過程當中執行的。理解了這個,那麼你就應該知道下面爲什麼是這樣的結果了:
1 console.log( a );//undefined 2 var a = 2;
其執行效果以下:
1 var a; 2 console.log( a );//undefined
3 a = 2;
咱們看到,變量聲明提早了,這就是爲何叫變量名提高了。因此在編譯階段,編譯器會將函數裏全部的聲明都提早到函數體內的上部,而真正賦值的操做留在原來的位置上,這也就是上面的代碼打出undefined的緣由。須要注意的是,變量名提高是以函數爲界的,嵌套函數內聲明的變量不會提高到外部函數體的上部。但願你懂這個概念了,若是不懂,能夠參考我以前寫的《也談談規範JS代碼的幾個注意點》及評論回答部分。
五、閉包
瞭解這些了後,咱們來聊聊閉包。什麼叫閉包?簡單的說就是一個函數內嵌套另外一個函數,這就會造成一個閉包。這樣提及來可能比較抽象,那麼咱們就舉例說明。可是在距離以前,咱們再複習下這句話,來,跟着大聲讀一遍,「不管函數是在哪裏調用,也不管函數是如何調用的,其肯定的詞法做用域永遠都是在函數被聲明的時候肯定下來的」。
1 function foo() { 2 var a = 2; 3 function bar() { 4 console.log( a ); // 2 5 } 6 bar(); 7 } 8 foo();
咱們看到上面的函數foo裏嵌套了bar,這樣bar就造成了一個閉包。在bar內能夠訪問到任何屬於foo的做用域內的變量。好,咱們看下一個例子:
1 function foo() { 2 var a = 2; 3 function bar() { 4 console.log( a ); 5 } 6 return bar; 7 } 8 var baz = foo(); 9 baz(); // 2
在第8行,咱們執行完foo()後按說垃圾回收器會釋放foo詞法做用域裏的變量,然而沒有,當咱們運行baz()的時候依然訪問到了foo中a的值。這是由於,雖然foo()執行完了,可是其返回了bar並賦給了baz,bar依然保持着對foo造成的做用域的引用。這就是爲何依然能夠訪問到foo中a的值的緣由。再想一想,咱們那句話,「不管函數是在哪裏調用,也不管函數是如何調用的,其肯定的詞法做用域永遠都是在函數被聲明的時候肯定下來的」。
來,下面咱們看一個經典的閉包的例子:
1 for (var i=1; i<10; i++) { 2 setTimeout( function timer(){ 3 console.log( i ); 4 },1000 ); 5 }
運行的結果是啥捏?你可能期待每隔一秒出來一、二、3...10。那麼試一下,按F12,打開console,將代碼粘貼,回車!咦???等一下,擦擦眼睛,怎麼會運行了10次10捏?這是腫麼回事呢?咋眼睛還很差使了呢?不要着急,等我給你忽悠!
如今,再看看上面的代碼,因爲setTimeout是異步的,那麼在真正的1000ms結束前,其實10次循環都已經結束了。咱們能夠將代碼分紅兩部分分紅兩部分,一部分處理i++,另外一部分處理setTimeout函數。那麼上面的代碼等同於下面的:
1 // 第一個部分 2 i++; 3 ... 4 i++; // 總共作10次 5 6 // 第二個部分 7 setTimeout(function() { 8 console.log(i); 9 }, 1000); 10 ... 11 setTimeout(function() { 12 console.log(i); 13 }, 1000); // 總共作10次
看到這裏,相信你已經明白了爲何是上面的運行結果了吧。那麼,咱們來找找如何解決這個問題,讓它運行如咱們所料!
由於setTimeout中的匿名function沒有將 i 做爲參數傳入來固定這個變量的值, 讓其保留下來, 而是直接引用了外部做用域中的 i, 所以 i 變化時, 也影響到了匿名function。其實要讓它運行的跟咱們料想的同樣很簡單,只須要將setTimeout函數定義在一個單獨的做用域裏並將i傳進來便可。以下:
1 for (var i=1; i<10; i++) { 2 (function(){ 3 var j = i; 4 setTimeout( function timer(){ 5 console.log( j ); 6 }, 1000 ); 7 })(); 8 }
不要激動,勇敢的去試一下,結果確定如你所料。那麼再看一個實現方案:
1 for (var i=1; i<10; i++) { 2 (function(j){ 3 setTimeout( function timer(){ 4 console.log( j ); 5 }, 1000 ); 6 })( i ); 7 }
啊,竟然這麼簡單啊,你確定在這麼想了!那麼,看一個更優雅的實現方案:
1 for (let i=1; i<=10; i++) { 2 setTimeout( function timer(){ 3 console.log( i ); 4 }, 1000 ); 5 }
咦?!腫麼回事呢?是否是出錯了,不着急,我這裏也出錯了。這是由於let須要在strict mode中執行。具體如何使用strict mode模式,自行谷歌吧!
六、運用
撤了這麼多,你確定會說,這TM都是廢話啊!囧,那麼下面就給你講一個用處的例子吧,也做爲本文的結束,也做爲一個思考題留給你,看看那裏用到了閉包及好處。
1 function Person(name) { 2 function getName() { 3 console.log( name ); 4 } 5 return { 6 getName: getName 7 }; 8 } 9 var littleMing = Person( "fool" ); 10 littleMing.getName();
哎,碼了個把小時文字,也是挺累的啊!湊巧你看到這個文章了,又湊巧以爲有用,贊一個唄!(歡迎吐槽!)