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

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