1.1 理解作用域
理解作用域
變量的賦值操作會執(zhí)行兩個動作,首先編譯器會在當(dāng)前作用域中聲明一個變量(如果沒有聲明過),然后在運行時引擎會作作用域中查找該變量,如果能找到就會對它賦值。
當(dāng)變量出現(xiàn)在賦值操作的左側(cè)時進行LHS查詢,右側(cè)進行RHS查詢。
RHS查詢與簡單地查找某個變量的值別無二致。即賦值的源頭
LHS查詢則是試圖找到變量的容器本身,從而可對其賦值。即賦值目標(biāo)
作用域嵌套
引擎從當(dāng)前的執(zhí)行作用域開始查找變量,如找不到,向上查找。當(dāng)?shù)诌_最外層全局作用域時,無論找到還是沒找到,都會停止。
不成功的RHS引用會導(dǎo)致拋出
ReferenceError異常。不成功的LHS引用會導(dǎo)致自動隱式地創(chuàng)建一個全局變量(非嚴(yán)格模式下),該變量使用LHS引用的目標(biāo)作為標(biāo)識符,或者拋出ReferenceError異常(嚴(yán)格模式下)
1.2 詞法作用域
詞法階段
詞法作用域是由你在寫代碼時將變量和塊作用域?qū)懺谀睦飦頉Q定的,因此當(dāng)詞法分析器處理代碼時會保持作用域不變。

- 包含著整個全局作用域,其中只有一個標(biāo)識符
foo - 包含著
foo所創(chuàng)建的作用域,其中有三個標(biāo)識符:abarb - 包含著
bar所創(chuàng)建的作用域,其中只有一個標(biāo)識符:c
作用域氣泡由其對應(yīng)的作用域塊代碼寫在哪里決定,它們是逐級包含的。
無論函數(shù)在
哪里被調(diào)用,也無論它如何被調(diào)用,它的詞法作用域都只由函數(shù)被聲明是所處的位置決定。
1.3 函數(shù)作用域和塊作用域
函數(shù)中的作用域
函數(shù)作用域的含義是指,屬于這個函數(shù)的全部變量都可在整個函數(shù)的范圍內(nèi)使用及復(fù)印。
隱藏內(nèi)部實現(xiàn)
可把變量和函數(shù)包裹在一個函數(shù)的作用域中,然后用這個作用域來"隱藏"它們。即最小授權(quán)或最小暴露原則。
“隱藏”作用域中的變量和函數(shù)所帶來的另一個好處,是可避免同名標(biāo)識符之間的沖突。
函數(shù)作用域
我們已知道,在任意代碼片段外加包裝函數(shù),可將內(nèi)部的變量和函數(shù)“隱藏”起來。如下:
var a = 2;
function foo() {
var a = 3;
console.log(a); //3
}
foo();
console.log(a); //2
但是并不理想,如果函數(shù)不需要函數(shù)名,并且能自動運行,這將會更加理想。
var a = 2;
(function foo(){
var a = 3;
console.log(a); //3
})();
console.log(a); //2
包裝函數(shù)的聲明以 (function...而不是以function...開始。函數(shù)會被當(dāng)作函數(shù)表達式而不是一個函數(shù)聲明來處理。
區(qū)分函數(shù)聲明和表達式最簡單的方法是看
funciton關(guān)鍵字出現(xiàn)在聲明中的位置。如果function是聲明中的第一個詞,就是函數(shù)聲明,否則就是一個函數(shù)表達式。
函數(shù)聲明和函數(shù)表達式之間最重要的區(qū)別是它們的名稱標(biāo)識符將會綁定的何處。
第一個片段中foo被綁定在所在作用域中,可直接通過foo()來調(diào)用調(diào)。
第二個片段中foo被綁定在函數(shù)表達式自身的函數(shù)中而不是所在作用域中。
換句話說(function foo(){...})作為函數(shù)表達式意味著foo只能在...處被訪問,外部作用域則不行。
匿名和具名
setTimeout( function() {
console.log("I waited 1 second!");
}, 1000)
這是匿名函數(shù)表達式
始終給函數(shù)表達式命名是一個最佳實踐。
IIFE
var a = 2;
(function IIFE(global){
var a = 3;
console.log( a ); // 3
console.log(global.a); // 2
})(window);
console.log(a); // 2
塊作用域
try/catch
try/catch的catch分句會創(chuàng)建一個塊作用域
let
let 關(guān)鍵字可將變量綁定到所在的任意作用域中(通常是{..}內(nèi)部)。
1.4 提升
變量和函數(shù)聲明會被提升,但是函數(shù)表達式不會被提升。
函數(shù)聲明會優(yōu)先被提升,然后才是變量
應(yīng)盡可能避免在塊內(nèi)部聲明函數(shù)。
1.5 作用域閉包
當(dāng)函數(shù)可記住并訪問所在的詞法作用域時,就產(chǎn)生了閉包,即使函數(shù)是在當(dāng)前詞法作用域之外執(zhí)行。
function foo(){
var a = 2;
function bar() {
console.log(a); //2
}
}
foo();
這是閉包嗎?
技術(shù)上來講,也許是。根據(jù)上面定義,確切地說并不是。
bar() 對 a 的引用的方法是詞法作用域查找規(guī)則,而這些規(guī)則只是閉包的一部分。
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz(); // 2 這就是閉包
bar()可被正常執(zhí)行,這個例子中,它在自己定義的詞法作用域以外的地方執(zhí)行。
在foo() 執(zhí)行后,通常會期待foo()的整個內(nèi)部作用域都被銷毀。而閉包的“神奇”之處正是可阻止這件事情的發(fā)生。事實上內(nèi)部作用域依然存在,因此沒有被回收。誰在使用這個內(nèi)部作用域?原來是bar()本身在使用。
bar()依然持有對該作用域的引用,而這個引用就叫做閉包。
無論使用何種方式對函數(shù)類型的值進行傳遞,當(dāng)函數(shù)在別處被調(diào)用時都可觀察到閉包。
function foo() {
var a = 2;
function baz() {
console.log(a); //2
}
bar(baz);
}
function bar(fn){
fn(); // 這就是閉包
}
傳遞函數(shù)當(dāng)然也可是間接的
var fn;
function foo(){
var a = 2;
function baz() {
console.log(a);
}
fn = baz; // 將baz分配給全局變量
}
function bar() {
fn(); // 這就是閉包
}
foo();
bar(); //2
無論通過何種手段將內(nèi)部函數(shù)傳遞到所在詞法作用域以外,它都會持有對原始定義作用域的引用,無論在何處執(zhí)行這個函數(shù)都會使用閉包。
現(xiàn)在我懂了
在定時器,事件監(jiān)聽器、Ajax請求、跨窗口通信、Web Workers 或者任何其他的異步(或同步)任務(wù)中,只要使用了回調(diào)函數(shù),實際上就是在使用閉包!
循環(huán)和閉包
for ( var i = 1; i <=5; i++) {
setTimeout(function timer(){
console.log( i );
}, i * 1000);
}
正常情況下,預(yù)期是分別輸出數(shù)字1~5。每秒一次
但是,會每秒一次輸出五次6
這是為什么?
首先6從哪來的呢?這個循環(huán)的終止條件是 i 不再 <=5。 條件首次成立時i 是6。因此,輸出顯示的是循環(huán)結(jié)束時 i 的最終值。
缺陷是我們試圖假設(shè)循環(huán)中的每個迭代在運行時會給自己“捕獲”一個i的副本。但是根據(jù)作用域的工作原理,實際情況是盡管循環(huán)中的五個函數(shù)是在各個迭代中分別定義的,但是它們都被封閉在一個共享的全局作用域中,因此實際上只有一個 i。
每個函數(shù)需要自己的變量,用來在每個迭代中儲存 i 的值。
for(var i=1; i<=5; i++) {
(function(j){
setTimeout( function timer() {
console.log( j );
}, j * 1000);
})( i );
}
重返塊作用域
let 聲明,可用來劫持塊作用域,并且在這個塊作用域中聲明一個變量。
for(let i = 1; i<=5; i++){
setTimeout( function timer(){
console.log( i );
}, i * 1000)
}