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


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

  scope和closure是javascript中兩個非常關鍵的概念,前者JS用多了還比較好理解,closure就不一樣了。我就被這個概念困擾了很久,無論看別人如何解釋,就是不通。不過理越辯越明,代碼寫的多了,小程序測試的多了,再回過頭看看別人寫的帖子,也就漸漸明白了閉包的含義了。咱不是啥大牛,所以不搞的那么專業了,唯一的想法就是試圖讓你明白什么是作用域,什么是閉包。如果看了這個帖子你還不明白,那么多寫個把月代碼回過頭再看,相信你一定會有收獲;如果看這個帖子讓你收獲到了一些東西,告訴我,還是非常開森的。廢話不多說,here we go!


 

  1、function

  在開始之前呢,先澄清一點(廢話咋這么多捏),函數在JavaScript中是一等公民。什么,你聽了很多遍了?!!!。那這里我需要你明白的是,函數在JavaScript中不僅可以調用來調用去,它本身也可以當做值傳遞來傳遞去的。


 

  2、scope及變量查詢

  作用域,也就是我們常說的詞法作用域,說簡單點就是你的程序存放變量、變量值和函數的地方。

  塊級作用域

  如果你接觸過塊級作用域,那么你應該非常熟悉塊級作用域。簡單說來就是,花括號{}括起來的代碼共享一塊作用域,里面的變量都對內或者內部級聯的塊級作用域可見。

  基於函數的作用域

  在JavaScript中,作用域是基於函數來界定的。也就是說屬於一個函數內部的代碼,函數內部以及內部嵌套的代碼都可以訪問函數的變量。如下:

  上面定義了一個函數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!

  


  3、scope的如何確定

  無論函數是在哪里調用,也無論函數是如何調用的,其確定的詞法作用域永遠都是在函數被聲明的時候確定下來的。理解這一點非常重要。


  4、變量名提升

  這也是個非常重要的概念。理解這個概念前,需要了解的是,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代碼的幾個注意點》及評論回答部分。


  5、閉包

  了解這些了后,我們來聊聊閉包。什么叫閉包?簡單的說就是一個函數內嵌套另一個函數,這就會形成一個閉包。這樣說起來可能比較抽象,那么我們就舉例說明。但是在距離之前,我們再復習下這句話,來,跟着大聲讀一遍,“無論函數是在哪里調用,也無論函數是如何調用的,其確定的詞法作用域永遠都是在函數被聲明的時候確定下來的”。

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 }

  運行的結果是啥捏?你可能期待每隔一秒出來1、2、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模式,自行谷歌吧!


  6、運用

  撤了這么多,你肯定會說,這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();

 


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

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM