一、javascript中的作用域
①全局變量-函數體外部進行聲明
②局部變量-函數體內部進行聲明
1)函數級作用域
javascript語言中局部變量不同於C#、Java等高級語言,在這些高級語言內部,采用的塊級作用域中會聲明新的變量,這些變量不會影響到外部作用域。
而javascript則采用的是函數級作用域,也就是說js創建作用域的單位是函數。
例如:
在C#當中我們寫如下代碼:
static void Main(string[] args) { for (var x = 1; x < 10; x++) { Console.WriteLine(x.ToString()); } Console.WriteLine(x.ToString()); }
上面代碼會出現如下的編譯錯誤:
The name 'x' does not exist in the current context
同樣在javascript當中寫如下代碼:
<script> function main() { for (var x = 1; x < 10; x++) { console.log(x.toString()); } console.log(x.toString()); } main(); </script>
輸出結果如下:
[Web浏覽器] "1"
[Web浏覽器] "2"
[Web浏覽器] "3"
[Web浏覽器] "4"
[Web浏覽器] "5"
[Web浏覽器] "6"
[Web浏覽器] "7"
[Web浏覽器] "8"
[Web浏覽器] "9"
[Web浏覽器] "10"
這就說明了,“塊級作用域”和“函數級作用域”的區別,塊級作用域當離開作用域後,外部就不能用了,就像上面的C#例子,"x"離開for循環後就不能用了,但是javascript中不一樣,它還可以進行訪問。
2)變量提升
再看javascript中作用域的一個特性,例子如下:
function func(){ console.log(x); var x = 1; console.log(x); } func();
輸出結果:
[Web浏覽器] "undefined"
[Web浏覽器] "1"
如上面的結果:有意思的是,為什麼第一個輸出是“undefined”呢?這就涉及到javascript中的“變量提升”,其實我感覺叫“聲明提升”更好,因為這個機制就是把變量的聲明提前到函數的前面。並不會把值也同樣提升,也就是為什麼第一次沒有輸出“1”的原因。
上面的代碼就相當於:
function func(){ var x; console.log(x); var x = 1; console.log(x); } func();
3)函數內部用不用“var”對程序的影響
這是個值得注意的地方:
var x = 1; function addVar(){ var x = 2; console.log(x); } addVar(); console.log(x);
輸出:
[Web浏覽器] "2"
[Web浏覽器] "1"
當在函數內部去掉var之後,再執行:
var x = 1; function delVar(){ x = 2; console.log(x); } delVar(); console.log(x);
[Web浏覽器] "2"
[Web浏覽器] "2"
上面的例子說明了,在函數內部如果在聲明變量沒有使用var,那麼聲明的變量就是全局變量。
二、javascript的作用域鏈
先看如下的代碼:
var name="Global"; function t(){ var name="t_Local"; function s1(){ var name="s1_Local"; console.log(name); } function s2(){ console.log(name); } s1(); s2(); } t();
輸出結果:
[Web浏覽器] "s1_Local"
[Web浏覽器] "t_Local"
那麼就有幾個問題:
1.為什麼第二次輸出的不是s1_Local?
2.為什麼不是Global?
解決這個兩個問題就在於作用域鏈…
下面就解析一下這個過程,
例如當上面程序創建完成的時候,會形成上圖中的鏈狀結構(假想的),所以每次調用s1()函數的時候,console.log(name);先會在他自己的作用域中尋找name這個變量,這裡它找到name=“s1_Local”,所以就輸出了s1_Local,而每次調用s2()的時候,它和s1()函數過程一樣,只不過在自身的作用域沒有找到,所以向上層查找,在t()函數中找到了,於是就輸出了"t_Local"。
同樣如果我們可以驗證一下,如果把t中的name刪除,可以看看輸出是不是“Global”
結果如下:
[Web浏覽器] "s1_Local"
[Web浏覽器] "Global"
當然具體每一個作用域直接是如何連接的,請參考
http://www.cnblogs.com/lhb25/archive/2011/09/06/javascript-scope-chain.html
其中也說明了為什麼JS當中應該盡量減少with關鍵字的使用。
三、閉包
了解了作用域和作用域鏈的概念之後,再去理解閉包就相對容易了。
1.閉包第一個作用
還是先看例子:
function s1() { var x = 123; return s2(); } function s2() { return x; } alert(s1());
這裡我想彈出x的值,但是卻發生了錯誤 "Uncaught ReferenceError: x is not defined",根據作用域鏈的知識,了解到因為s2中沒有x的定義,並且向上找全局也沒有x定義,所以就報錯了。也就是說s1和s2不能夠共享x的值。
那麼問題來了,我想要訪問s1當中的x,怎麼弄?
修改代碼如下:
function s1() { var x = 123; return function(){ return x; }; } var test = s1(); console.log(test());
結果為:
[Web浏覽器] "123"
解釋:因為function本質也是數據,所以它和x的作用域相同,可以訪問x。這樣就相當於對外部開放了一個可以訪問內部變量的接口。
還可以怎麼玩,稍微修改一下代碼
var func; function f(){ var x='123'; func=function(){ return x; }; } f(); alert(func());
定義一個全局的變量,然後在函數內部讓其等於一個函數,這樣就可以通過這個全局變量來進行訪問x了。
綜上:閉包是啥?閉包就相當於函數外部和內部的橋梁,通過閉包可以在外部訪問函數內部的變量。這也是閉包的第一個作用。
2.閉包的第二個作用
先看代碼:
function f1(){ var n=1; add=function(){ n+=1; }; function f2(){ console.log(n); return '輸出完成'; } return f2; } var res=f1(); console.log(res()); add(); console.log(res());
輸出結果:
[Web浏覽器] "1"
[Web浏覽器] "輸出完成"
[Web浏覽器] "2"
[Web浏覽器] "輸出完成"
問題為什麼第二次輸出的結果n變成了2,沒有被清除?
我的理解:res是一個全局變量,一直駐留在內存當中,它就相當於f2函數,f2函數又要依賴於f1函數,所以f1也駐留在內存當中,保證不被GC所回收,每一次調用add函數的時候,就相當於把f1中的n重新賦值了,這樣n的值就變化了。這也是閉包的第二個作用,可以讓變量的值始終保存在內存當中。
3.閉包的應用
①可以做訪問控制(相當於C#當中的屬性)
//定義兩個變量,用於存儲取值和存值 var get,set; //定義一個自調用函數,設定set和get方法 (function(){ //設定x的默認值 var x = 0; set = function(n){ x = n; } get = function(){ return x; } })(); console.log(get()); set(5); console.log(get());
輸出結果:
[Web浏覽器] "0"
[Web浏覽器] "5"
②可以用做迭代器
//定義一個函數,裡面使用了閉包 function foo(myArray){ var i=0; //閉包迭代器 next=function(){ //每次返回數組的當前值,下標+1 return myArray[i++]; } } //調用foo,參數為一個數組 foo(['a','b','c','d']); //每次調用next都會打印數組的一個值 console.log(next()); console.log(next()); console.log(next()); console.log(next());
輸出結果:
[Web浏覽器] "a"
[Web浏覽器] "b"
[Web浏覽器] "c"
[Web浏覽器] "d"
③閉包在循環中的使用
例1 function f(){ var a=[]; var i; for(i=0;i<3;i++){ a[i]=function(){ return i; }; } return a; } var test=f(); console.log(test[0]()); console.log(test[1]()); console.log(test[2]());
輸出結果:
[Web浏覽器] "3"
[Web浏覽器] "3"
[Web浏覽器] "3"
為什麼結果不是0、1、2?
這裡我們使用了閉包,每次循環都指向了同一個局部變量i,但是閉包不會記錄每一次循環的值,只保存了變量的引用地址,所以當我們在調用test[0]()、test[1]()、test[2]()的時候都返回的是for循環最後的值,也就是3的時候跳出了循環。
例2:我想輸出0,1,2怎麼搞?
function f(){ var a=[]; var i; for(i=0;i<3;i++){ a[i]=(function(x){ return function(){ return x; } })(i); } return a; } var test=f(); console.log(test[0]()); console.log(test[1]()); console.log(test[2]());
結果:
[Web浏覽器] "0"
[Web浏覽器] "1"
[Web浏覽器] "2"
關於這個為什麼和上面不一樣,我在知乎上找到了一篇文章,總結的非常好,