1.什麼是作用域(scope)? 簡單來講,作用域(scope)就是變數訪問規則的有效範圍。 作用域外,無法引用作用域內的變數; 離開作用域後,作用域的變數的記憶體空間會被清除,比如執行完函數或者關閉瀏覽器 作用域與執行上下文是完全不同的兩個概念。我曾經也混淆過他們,但是一定要仔細區分。 JavaSc ...
1.什麼是作用域(scope)?
簡單來講,作用域(scope)就是變數訪問規則的有效範圍。
- 作用域外,無法引用作用域內的變數;
- 離開作用域後,作用域的變數的記憶體空間會被清除,比如執行完函數或者關閉瀏覽器
- 作用域與執行上下文是完全不同的兩個概念。我曾經也混淆過他們,但是一定要仔細區分。
JavaScript代碼的整個執行過程,分為兩個階段,代碼編譯階段與代碼執行階段。編譯階段由編譯器完成,將代碼翻譯成可執行代碼,這個階段作用域規則會確定。執行階段由引擎完成,主要任務是執行可執行代碼,執行上下文在這個階段創建。
函數作用域是在函數聲明的時候就已經確定了,而函數執行上下文是在函數調用時創建的。假如一個函數被調用多次,那麼它就會創建多個函數執行上下文,但是函數作用域顯然不會跟著函數被調用的次數而發生什麼變化。
1.1 全局作用域
var foo = 'foo'; console.log(window.foo); // => 'foo'
在瀏覽器環境中聲明變數,該變數會預設成為window對象下的屬性。
function foo() { name = "bar" } foo(); console.log(window.name) // bar
在函數中,如果不加 var 聲明一個變數,那麼這個變數會預設被聲明為全局變數,如果是嚴格模式,則會報錯。
全局變數會造成命名污染,如果在多處對同一個全局變數進行操作,那麼久會覆蓋全局變數的定義。同時全局變數數量過多,非常不方便管理。
這也是為什麼jquery要在全局建立變數 ,其餘私有方法屬性掛在,其餘私有方法屬性掛在 下的原因。
1.2 函數作用域
假如在函數中定義一個局部變數,那麼該變數只可以在該函數作用域中被訪問。
function doSomething () { var thing = '吃早餐'; } console.log(thing); // Uncaught ReferenceError: thing is not defined
嵌套函數作用域:
function outer () { var thing = '吃早餐'; function inner () { console.log(thing); } inner(); } outer(); // 吃早餐
在外層函數中,嵌套一個內層函數,那麼這個內層函數可以向上訪問到外層函數中的變數。
既然內層函數可以訪問到外層函數的變數,那如果把內層函數return出來會怎樣?
function outer () { var thing = '吃早餐'; function inner () { console.log(thing); } return inner; } var foo = outer(); foo(); // 吃早餐
函數執行完後,函數作用域的變數就會被垃圾回收。而這段代碼看出當返回了一個訪問了外部函數變數的內部函數,最後外部函數的變數得以保存。
這種當變數存在的函數已經執行結束,但扔可以再次被訪問到的方式就是“閉包”。後期會繼續對閉包進行梳理。
1.3 塊級作用域
很多書上都有一句話,javascript沒有塊級作用域的概念。所謂塊級作用域,就是{}包裹的區域。但是在ES6出來以後,這句話並不那麼正確了。因為可以用 let 或者 const 聲明一個塊級作用域的變數或常量。
比如:
for (let i = 0; i < 10; i++) { // ... } console.log(i); // Uncaught ReferenceError: i is not defined
發現這個例子就會和函數作用域中的第一個例子一樣的錯誤提示。因為變數i只可以在 for迴圈的{ }塊級作用域中被訪問了。
擴散思考:
究竟什麼時候該用let?什麼時候該用const?
預設使用 const,只有當確實需要改變變數的值的時候才使用let。因為大部分的變數的值在初始化之後不應再改變,而預料之外的變數的修改是很多bug的源頭。
1.4 詞法作用域
詞法作用域,也可以叫做靜態作用域。意思是無論函數在哪裡調用,詞法作用域都只在由函數被聲明時所處的位置決定。
既然有靜態作用域,那麼也有動態作用域。
而動態作用域的作用域則是由函數被調用時執行的位置所決定。
var a = 123; function fn1 () { console.log(a); } function fn2 () { var a = 456; fn1(); } fn2(); // 123
以上代碼,最後輸出結果 a 的值,來自於 fn1 聲明時所在位置訪問到的 a 值 123。
所以JS的作用域是靜態作用域,也叫詞法作用域。
上面的1.1-1.3可以看做作用域的類型。而這一小節,其實跟上面三小節還是有差別的,並不屬於作用域的類型,只是關於作用域的一個補充說明吧。
2. 什麼是作用域鏈(scope chain)
在JS引擎中,通過標識符查找標識符的值,會從當前作用域向上查找,直到作用域找到第一個匹配的標識符位置。就是JS的作用域鏈。
var a = 1; function fn1 () { var a = 2; function fn2 () { var a = 3; console.log(a); } fn2 (); } fn1(); // 3
console.log(a) 語句中,JS在查找 a變數標識符的值的時候,會從 fn2 內部向外部函數查找變數聲明,它發現fn2內部就已經有了a變數,那麼它就不會繼續查找了。那麼最終結果也就會列印3了。
代碼分析如下:
<script type="text/javascript"> var a = 100; function fun(){ var b = 200 function fun2(){ var c = 300 } function fun3(){ var d = 400 } fun2() fun3() } fun() </script>
首先預編譯,一開始生成一個GO{
a:underfined
fun:function fun(){//fun的函數體
var b = 200
function fun2(){
var c = 300
}
function fun3(){
var d = 400
}
fun2()
fun3()
}
}
逐行執行代碼,GO{
a:100
fun:function fun(){//fun的函數體
var b = 200
function fun2(){
var c = 300
}
function fun3(){
var d = 400
}
fun2()
fun3()
}
}
當fun函數執行時,首先預編譯會產生一個AO{
b:underfined
fun2:function fun2(){
var c = 300
}
fun3:function fun3(){
var d = 400
}
}
這裡註意的是fun函數是在全局的環境下產生的,所以自己身上掛載這一個GO,由於作用域鏈是棧式結構,先產生的先進去,最後出來,
在這個例子的情況下,AO是後於GO產生的,所以對於fun函數本身來說,執行代碼的時候,會先去自己本身的AO里找找看,如果沒有找到要用的東西,就去父級查找,此題的父級是GO
此刻fun的作用域鏈是 第0位 fun的AO{}
第1位 GO{}
fun函數開始逐行執行AO{
b:200
fun2:function fun2(){
var c = 300
}
fun3:function fun3(){
var d = 400
}
}
註意:函數每次調用才會產生AO,每次產生的AO還都是不一樣的
然後遇到fun2函數的執行,預編譯產生自己的AO{
c:underfined
}
此刻fun2的作用域鏈是第0位 fun2的AO{}
第1位 fun的AO{}
第2位 GO{}
然後遇到fun3函數的執行,預編譯產生自己的AO{
d:underfined
}
此刻fun3的作用域鏈是第0位 fun3的AO{}
第1位 fun的AO{}
第2位 GO{}
fun2和fun3的作用域鏈沒有什麼聯繫。
當函數fun2和fun3執行完畢,自己將砍掉自己和自己的AO的聯繫,
最後就是fun函數執行完畢,它也是砍掉自己和自己AO的聯繫。
這就是一個我們平時看到不是閉包的函數。
閉包
1.閉包在紅寶書中的解釋就是:有權訪問另一個函數作用域中的變數的函數。
2.寫法:
1 <script type="text/javascript"> 2 function fun1(){ 3 var a = 100; 4 function fun2(){ 5 a++; 6 console.log(a); 7 } 8 return fun2; 9 } 10 11 var fun = fun1(); 12 fun() 13 fun() 14 </script>
3.效果如下:
4.分析:
執行代碼
GO{
fun:underfined
fun1:function fun1()
{
var a = 100;
function fun2()
{
a++;
console.log(a);
}
return fun2;
}
}
然後第十一行開始這裡,就是fun1函數執行,然後把fun1的return返回值賦給fun,這裡比較複雜,我們分開來看,
這裡fun1函數執行,產生AO{
a:100
fun2:function fun2(){
a++;
console.log(a);
}
}
此刻fun1的作用域鏈為 第0位 AO
第1位 GO
此刻fun2的作用域鏈為 第0位 fun1的AO
第1位 GO
解釋一下,fun2只是聲明瞭,並沒有產生調用,所以沒有產生自己的AO,
正常的,我們到第7行代碼我們就結束了,但是這個時候來了一個return fun2,把fun2這個函數體拋給了全局變數fun,好了,fun1函數執行完畢,消除自己的AO,
此刻fun2的作用域鏈為 第0位 fun1的AO
第1位 GO
第十二行就是fun執行,然後,它本身是沒有a的,但是它可以用fun1的AO,然後加,然後列印,
因為fun中的fun1的AO本來是應該在fun1銷毀時,去掉,但是被拋給fun,所以現在fun1的AO沒辦法銷毀,所以現在a變數相當於一個只能被fun訪問的全局變數。
所以第十三行再調用一次fun函數,a被列印的值為102。