1 作用域和閉包

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)詞法分析器處理代碼時會保持作用域不變。

image.png
  1. 包含著整個全局作用域,其中只有一個標(biāo)識符 foo
  2. 包含著foo所創(chuàng)建的作用域,其中有三個標(biāo)識符: a bar b
  3. 包含著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)
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容