作用域
作用域就是變量起作用的范圍。作用域包括全局作用域,函數作用域以塊級作用域,ES6中的let和const可以形成塊級作用域。
除了塊級作用域,在函數外面聲明的變量可以在任何一個地方被訪問到,這些變量的作用域都是全局作用域,全局作用域中的變量可以再任何一個地方使用:
var a = "zt"; function fn1(){ console.log(a); } function fn2(){ console.log(a); } fn1(); fn2();
在函數裡面聲明的變量只能在當前函數內使用,這些變量的作用域我們稱為函數作用域,只在當前函數內有效:
function fn1(){ var a = "zt"; console.log(a); } function fn2(){ console.log(a) } fn1(); fn2();//報錯提示a沒有定義
函數內定義的變量只在當前函數內有效,在函數以外的地方是不能被訪問到的,fn2函數內沒有定義a,全局作用域中也沒有a使用一個不存在的變量所以報錯。
作用域鏈
作用域是可以嵌套的比如在全局作用域裡面創建一個函數,函數裡面可以在創建一個函數,這樣就發生了作用域的嵌套,作用域鏈可以把作用域鏈接起來。當使用一個變量的時候,會優先在當前作用域內去尋找變量,如果當前作用域內不存在就會去上層作用域去尋找一直到全局作用域,如果還不能找到變量就會報錯。
var a = "global"; function fn1(){ console.log(a); } fn1();
作用域是靜態的
我們先看一個例子:
var flag = "outer"; function demo(){ var flag = "inner"; function inner(){ console.log(flag); } return inner; } var fn = demo(); fn();//inner
var flag = "outer"; function demo(){ var flag = "inner"; fn(); } function fn(){ console.log(flag); } demo();//outer
通過這兩個例子我們可以看出函數的作用域是靜態的,一個函數不管在哪被調用,它的作用域都是聲明時的作用域。函數的作用域在聲明時就已經被創建,在調用函數時會去訪問他已經創建的作用域。
閉包
閉包在MDN中的定義為:閉包是指那些可以訪問獨立變量的函數,所以在定義上我們可以把所有的函數都看做是閉包。閉包即密閉的空間,我們可以很自然的想到函數,因為函數就會生成一個密閉的空間,如果函數想稱為一個閉包只需要在使用一個外部變量即可(使用外部變量的函數就是閉包)。通過閉包可以給我們帶來一些便利,就是可以在高等級的作用域使用低等級作用域中的變量:
function demo(){ var flag = "test"; return function(){ console.log(flag); } } demo()();
我們把demo函數裡面的函數通過return使其可以在外部使用,我們已經說過作用域都是靜態的,這樣我們在外部使用return的函數時,就可以看到我們在全局作用域中調用函數最後輸出了demo函數裡面的"test"。
這樣我們可以做一些更有意義的事:
var data = []; function demo(){ var data = []; return{ add:function(a){ data.push(a); }, print:function(){ console.log(data); } } } var tool = demo(); tool.add(1); tool.add(2); tool.add(3); tool.print();//[1, 2, 3]
我們可以利用demo函數裡面的data來存儲我們的信息而且不用擔心它被破壞(demo裡面的data被私有化),而且我們也可以在外部在聲明一個同名的data來存儲別的信息,這兩個不會產生任何沖突。
閉包也可以幫我們解決一些小問題:
for(var i=0;i<4;i++){ setTimeout(function(){ console.log(i); }); }
我們預期的結果是打印當前循環的i值結果輸出全是4。先解釋一下出現這麼情況的原因:JS是一種單線程的語言,而setTimeout是異步的,只有當我們的代碼執行完成以後setTimeout的處理函數才會執行,而執行的時候i的值已經是4了所以最終的輸出全是4。
我們可以通過閉包來解決這一問題:
for(var i=0;i<4;i++){ (function(i){ setTimeout(function(){ console.log(i) }) }(i)) }
閉包可以形成一個獨立的作用域這樣每次循環都會有一個獨立的函數作用域,循環完成後雖然i的值仍然是4但是setTimeout的處理函數在尋找i的時候會優先找到作為參數的i,而每一個參數i都表示當次循環的i,利用閉包我們可以完美的解決這種問題。
在我們實際開發的過程中,遇到這種情況我們就可以通過閉包來解決,我們所說的"這種情況"通常有三個特點:
1.首先有一個循環
2.循環裡面會創建函數,並且函數是延後執行的
3.這些延後執行的函數會使用一個共同的變量,並且這個共同的變量和當前的循環值有關系
我們按照這個規律套一下上面的代碼:
循環有了,每次循環也會生成一個函數,這些函數也都是在循環完成後才能執行,而且每一個函數都使用共同的i,而i就是當前的循環值,正好符合我們的三個特點。我們通過(function(){}())這種方式(匿名函數自執行)來形成一個閉包達到我們預期的目的。