聊一下JS中的做用域scope和閉包closure

聊一下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();

 


 哎,碼了個把小時文字,也是挺累的啊!湊巧你看到這個文章了,又湊巧以爲有用,贊一個唄!(歡迎吐槽!)

相關文章
相關標籤/搜索