聊一下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();
哎,碼了個把小時文字,也是挺累的啊!湊巧你看到這個文章了,又湊巧覺得有用,贊一個唄!(歡迎吐槽!)