在翻閱《你不知道的javascript》這一套書的中上卷目錄之后,發現書中針對閉包、對象、原型、語法、異步、回調等等既基礎又重要的
javascript知識有著針對性的闡述,于是決定對這套書的中上卷進行學習。上卷和中卷各講述了兩大部分知識,分別是:作用域與閉包、
this和對象原型、類型和語法、異步和性能。本文是對作用域與閉包的學習總結。
對于作用域及其相關知識的理解,我認為主要把握這樣一些東西:js所使用作用域的類型,IIFE以及作用域中的提升。
1.作用域類型
作用域分為詞法作用域和動態作用域,js使用的是詞法作用域。所謂詞法作用域,指的是在編譯詞法分析階段根據詞法單元確定下來的作用域,并且在引擎執行代碼階段作用域是不變的(大部分情況下),注意括號里的大部分情況,因為在通常對語法的學習中,只要注意一些不符合常規情況的狀態,對語法的把握會順利許多。
一小部分會在執行代碼期間改變作用域的情況(書中稱之為“欺騙”)是兩種:在js中使用了eval()和with()方法(在非嚴格模式下):
- eval()可以接受一個字符串作為參數,并且在執行代碼期間,將參數中可能傳遞的值看作本來就存在于eval()所在位置的代碼;
- with()主要作用是可以接受一個對象,并快捷引用其中的屬性。然而,with()在處理這個對象的時候,不僅會形成一個新的作用域,
還可能改變原本存在的詞法作用域(這個方法比較復雜)。
對eval()舉個例子:
function foo(str,a) {
eval(str);
console.log(a,b);
}
var b = 2;
foo("var b = 3",1);//1,3
在執行foo("var b = 3",1)語句時,在正常情況下,函數foo(str,a)執行到console.log(a,b)時,查找b會找到全局變量中的b并得到2,但實際卻得到3,這是因為eval()將“var b = 3”看作在函數foo(str,a)中的代碼,相當于在函數中聲明了一個b作為局部變量,于是下一句console.log(a,b)執行的時候先找到局部變量得到值3,就結束了查找,而全局變量b被屏蔽了。
在嚴格模式下,eval()有自己的作用域,不會在其所處函數中產生屏蔽效應,而with()是被禁用的。并且,在第一章中講到,引擎在優化性能的時候,是根據已經確定的作用域和詞法單元來進行優化的,在使用了eval()和with()后,使得函數作用域可能在動態情況下(執行代碼期間)發生變化,而引擎在遇到這個兩個方法后就不會進行性能優化。所以,在js中使用這兩個方法會導致性能下降。
對于詞法作用域,其中又分成了塊作用域和函數作用域,js絕大部分情況下使用的是函數作用域,但存在個別塊作用域的時候,使用with()和try~catch語句的時候會形成塊作用域(又是with(),在這里,《高程》第三版第4章中也講到了with()會形成作用域,但《高程》中提到這種情況使得作用域鏈變長了,而with()形成的依然是函數作用域),并且ES6出來之后,也正式承認了塊作用域的存在和使用,在{}中使用let和const聲明就能創造塊作用域了,這為使用循環語句等提供了很大的便利。
2.IIFE
IIFE是立即執行函數表達式。首先應該理解什么是函數表達式,非常簡單,以function單詞作為開頭的是函數聲明,而帶有function但并非以其開頭的語句就是函數表達式,最典型的情況就是(function(){})。函數聲明和函數表達式相同的地方在于形成了函數作用域,而不同的地方函數聲明會被提升,而表達式不會(顯然不會,因為聲明才是編譯器工作的對象)。
函數表達式的一個主要作用是可以使函數匿名,這樣就避免了函數作用域中多出一個標識符。但是并非所有情況下匿名都是最優的,例如:當函數不只需要被調用一次的時候,就不能匿名了。
而函數表達式另一個主要的作用就是作為IIFE的出現,既(function(){})()或(function(){}())的形式,可以立即執行函數,而無需調用或加載。而IIFE多出來的括號并不是只使得函數會立即執行,在括號中還可以傳遞需要的參數。例如,以(function(a,b){})(c,d)的形式既能將參數傳遞進函數了,而簡單的函數表達式(function(){})卻沒有這樣的功能。
此外,IIFE還是閉包機制的一個最佳實踐(雖然不是最能體現閉包機制的方式),在第三部分的閉包中會講到。
3.作用域中的提升
其實作用域中的提升非常簡單,只要能夠理解js的編譯原理:在編譯器工作階段,會進行聲明,以及對其他語句形成引擎執行的代碼,因此,在編寫的代碼中,不管一個作用域中的聲明處于哪里,肯定都比賦值等其他句語先完成,而js語句的執行是按照從上到下執行的,這就好比將聲明從下統一提升到了函數中最上面的地方,這就稱為聲明。
對提升需要把握的要點有以下三處:
- 函數表達式不會提升;
- let和const聲明不會提升;
- 函數聲明會提升到變量聲明的上方,而在同一個作用域中聲明了同名函數時,后一個同名函數在提升時會覆蓋前一個函數的聲明。