深入理解JavaScript中作用域問(wèn)題

JavaScript中什么是作用域幽邓?有多少種作用域距糖?為什么我要學(xué)習(xí)作用域?

相信很多小伙伴們都能明白“作用域”這三個(gè)字垒迂,按照字面上理解械姻,它不就是“發(fā)揮作用的區(qū)域”嗎?然而在我的前端學(xué)習(xí)中娇斑,我發(fā)現(xiàn)它有一個(gè)非常專(zhuān)業(yè)的解釋稱(chēng)為 執(zhí)行環(huán)境策添。

執(zhí)行環(huán)境是什么

執(zhí)行環(huán)境是JavaScript中最為重要的一個(gè)概念,因?yàn)樗x了變量或函數(shù)有權(quán)訪問(wèn)其他數(shù)據(jù)毫缆,并且決定了它們的行為,因此執(zhí)行環(huán)境擁有一套獨(dú)有的 規(guī)則乐导。接下來(lái)苦丁,我們?nèi)雮€(gè)門(mén)來(lái)了解下JavaScript是如何工作的?

編譯原理

JavaScript 引擎的編譯步驟物臂,可以說(shuō)是為我們后面理解執(zhí)行環(huán)境埋下了深厚的伏筆旺拉。在這里我將給大家介紹它的三個(gè)具體步驟:

  1. 詞法分析

    這個(gè)過(guò)程會(huì)將由字符組成的字符串分解成(對(duì)編程語(yǔ)言來(lái)說(shuō))有意義的代碼塊产上。這些代碼塊被稱(chēng)為詞法單元。
    例如: var a = 2;蛾狗,這個(gè)字符串你會(huì)怎么理解晋涣?通常這段字符串會(huì)被分解成為下面這些詞法單元:vara沉桌、=谢鹊、2;留凭〉瓒螅空格是否會(huì)被當(dāng)作詞法單元,取決于空格在這門(mén)語(yǔ)言中是否具有意義蔼夜。

  2. 語(yǔ)法分析

    這個(gè)過(guò)程是將詞法單元流(數(shù)組)轉(zhuǎn)換成一個(gè)由元素逐級(jí)嵌套所組成的代表了程序語(yǔ)法結(jié)構(gòu)的樹(shù)兼耀。這個(gè)樹(shù)被稱(chēng)為“抽象語(yǔ)法樹(shù)”(Abstract Syntax Tree,AST)求冷。

  3. 生成代碼

    最后這個(gè)過(guò)程是將“抽象語(yǔ)法樹(shù)”(AST) 轉(zhuǎn)換為可執(zhí)行代碼的過(guò)程稱(chēng)被稱(chēng)為代碼生成瘤运。這個(gè)過(guò)程與語(yǔ)言、目標(biāo)平臺(tái)等息息相關(guān)匠题。 拋開(kāi)具體細(xì)節(jié)拯坟,簡(jiǎn)單來(lái)說(shuō)就是有某種方法可以將 var a = 2; 的 AST 轉(zhuǎn)化為一組機(jī)器指令,用來(lái)創(chuàng)建一個(gè)叫作 a 的變量(包括分配內(nèi)存等)梧躺,并將一個(gè)值儲(chǔ)存在 a 中似谁。

小結(jié): var a = 2;,意思就是先在我的編譯器中聲明這個(gè)變量 a掠哥,并為它分配內(nèi)存巩踏,緊接著我再賦個(gè)值給 a,這個(gè)值存在a里面续搀。

三位好伙伴

要參與到對(duì)程序 var a = 2; 進(jìn)行處理的過(guò)程塞琼,我們需要了解三位好家伙,分別是引擎禁舷、編譯器彪杉、作用域∏A可以說(shuō) JavaScript 工作都是靠它們?nèi)淮蠊Τ紒?lái)完成了派近,首先 引擎 從頭到尾負(fù)責(zé)整個(gè) JavaScript 程序的編譯及執(zhí)行過(guò)程,緊接著 編譯器 負(fù)責(zé)語(yǔ)法分析及代碼生成等臟活累活洁桌,最后 作用域 負(fù)責(zé)收集并維護(hù)由所有聲明的標(biāo)識(shí)符(變量)組成的一系列查 詢渴丸,并實(shí)施一套非常嚴(yán)格的規(guī)則,確定當(dāng)前執(zhí)行的代碼對(duì)這些標(biāo)識(shí)符的訪問(wèn)權(quán)限。

下面我們將 var a = 2; 分解谱轨,看看引擎和它的朋友們是如何協(xié)同工作的戒幔。

  • 遇到 var a,編譯器會(huì)詢問(wèn)作用域是否已經(jīng)有一個(gè)該名稱(chēng)的變量存在于同一個(gè)作用域的集合中土童。如果是诗茎,編譯器會(huì)忽略該聲明,繼續(xù)進(jìn)行編譯献汗;否則它會(huì)要求作用域在當(dāng)前作用域的集合中聲明一個(gè)新的變量敢订,并命名為 a。
  • 接下來(lái)編譯器會(huì)為引擎生成運(yùn)行時(shí)所需的代碼雀瓢,這些代碼被用來(lái)處理 a = 2 這個(gè)賦值操作枢析。引擎運(yùn)行時(shí)會(huì)首先詢問(wèn)作用域,在當(dāng)前的作用域集合中是否存在一個(gè)叫作 a 的變量刃麸。如果是醒叁,引擎就會(huì)使用這個(gè)變量;如果否泊业,引擎會(huì)繼續(xù)查找該變量把沼。
  • 如果引擎最終找到了 a 變量,就會(huì)將 2 賦值給它吁伺。否則引擎就會(huì)舉手示意并拋出一個(gè)Error饮睬!

LHS & RHS

LHS(Left-Hand-Search)稱(chēng)為左側(cè)查找,那么顧名思義篮奄,RHS(Right-Hand-Search)自然叫做右側(cè)查找了捆愁。在概念上最好將其理解為“賦值操作的目標(biāo)是誰(shuí)(LHS)”以及“誰(shuí)是賦值操作的源頭 (RHS)”。

考慮以下代碼你會(huì)更明白:

function foo(a) {
            console.log(a); // 2 
        }
        foo(2); 

最后一行foo( 2 );我們調(diào)用了 foo 函數(shù)窟却,并且傳給它了一個(gè)實(shí)參 2昼丑,對(duì)于 foo 函數(shù),它接受到了一個(gè)參數(shù)并把這個(gè)參數(shù)賦值給 a夸赫,因此a = 2菩帝,這一步是左側(cè)查找了,可以理解為 a 是 2 要賦值的目標(biāo)茬腿。最后console.log( a );這段話中呼奢,執(zhí)行的就是右側(cè)查找,因?yàn)槲覀円敵?a 就必須去查找這個(gè)值切平。

小結(jié): LHS 指的是等式左邊部分握础,RHS 指的是等式右邊部分

有了以上的基礎(chǔ)概念,我們不難理解原來(lái)作用域就是我代碼書(shū)寫(xiě)悴品、生成弓候、和執(zhí)行的地方啊郎哭,也就是代碼的執(zhí)行環(huán)境啊他匪。

作用域嵌套

當(dāng)一個(gè)塊或函數(shù)嵌套在另一個(gè)塊或函數(shù)中時(shí)菇存,就發(fā)生了作用域的嵌套。因此邦蜜,在當(dāng)前作用域中無(wú)法找到某個(gè)變量時(shí)依鸥,引擎就會(huì)在外層嵌套的作用域中繼續(xù)查找,直到找到該變量悼沈, 或抵達(dá)最外層的作用域(也就是全局作用域)為止贱迟。

考慮以下代碼:

function foo(a) {
            console.log(a + b);
        }
        var b = 2;
        foo(2); // 4

你會(huì)發(fā)現(xiàn)在我的 foo 函數(shù)中,我并沒(méi)有聲明 a絮供、b 變量衣吠,怎么辦?輸出會(huì)報(bào)錯(cuò)嗎壤靶?把代碼運(yùn)行一下缚俏,結(jié)果出乎意料居然是 4。這個(gè)結(jié)果到底是怎么來(lái)的贮乳?

  1. 前面我們剛講過(guò) LHS 和 RHS忧换,所以我們能理解當(dāng)我們調(diào)用 foo 函數(shù)時(shí)其實(shí)是進(jìn)行了一次 LHS,把 2 賦值給了 a向拆。
  2. 對(duì)于 b 的值是如何取到的亚茬,我們就要明白如果引擎在當(dāng)前作用域中無(wú)法找到某個(gè)變量時(shí),它就會(huì)在外層嵌套的作用域中繼續(xù)查找浓恳,在這里 foo 函數(shù)中并沒(méi)有 b 這個(gè)變量刹缝,因此引擎會(huì)向外查找,下一個(gè)作用域就是全局作用域了颈将,很慶幸梢夯,在全局作用域中有 b 這個(gè)變量,這是 foo 函數(shù)就可以獲得 b 變量的值 2吆鹤,最后輸出 4 了厨疙。

作用域類(lèi)別

作用域有三種,分為詞法作用域疑务,函數(shù)作用域和塊作用域沾凄。當(dāng)然,你也可以說(shuō)作用域有兩種知允,靜態(tài)作用域和動(dòng)態(tài)作用域撒蟀,那么這又是另一個(gè)話題了。

詞法作用域

簡(jiǎn)單地說(shuō)温鸽,詞法作用域就是定義在詞法階段的作用域保屯。換句話說(shuō)手负,詞法作用域是由你在寫(xiě)代碼時(shí)將變量和塊作用域?qū)懺谀睦飦?lái)決定的。

考慮以下代碼:

function foo(a) {
            var b = a * 2;

            function bar(c) {
                console.log(a, b, c);
            }

            bar(b * 3);
        }

        foo(2); // 2, 4, 12

在這個(gè)例子中有三個(gè)逐級(jí)嵌套的作用域姑尺。為了幫助理解竟终,可以將它們想象成幾個(gè)逐級(jí)包含的氣泡。

image

查找 a切蟋、b统捶、c 依次從內(nèi)向外,a柄粹、b 都在 foo 函數(shù)作用域中找到喘鸟,c 在 bar 作用域中找到。作用域查是從最內(nèi)層的作用域開(kāi)始查找驻右,例如:bar -> foo -> global什黑,它會(huì)在找到第一個(gè)匹配的標(biāo)識(shí)符時(shí)停止。我們把 bar -> foo -> global 稱(chēng)為作用域鏈堪夭,引擎在查找時(shí)就是依照作用域鏈不斷向上查找的愕把。

函數(shù)作用域

函數(shù)作用域的含義是指,屬于這個(gè)函數(shù)的全部變量都可以在整個(gè)函數(shù)的范圍內(nèi)使用及復(fù)用(事實(shí)上在嵌套的作用域中也可以使用)茵瘾。

在學(xué)習(xí)詞法作用域的時(shí)候礼华,我們就已經(jīng)接觸到了函數(shù)作用域,但是我仍需要強(qiáng)調(diào)一點(diǎn)拗秘,函數(shù)作用域中的變量只能給自己和子函數(shù)使用圣絮,父級(jí)函數(shù)是獲取不到子函數(shù)中的變量的。

舉個(gè)例子:

function foo(a) {
            function bar(c) {
                c = 3;
            }
            console.log(a, c)
        }

        foo(2); // Uncaught ReferenceError: c is not defined

塊作用域

1. 零污染

盡管函數(shù)作用域是最常見(jiàn)的作用域單元雕旨,當(dāng)然也是現(xiàn)行大多數(shù) JavaScript 中最普遍的設(shè)計(jì)方法扮匠,但其他類(lèi)型的作用域單元也是存在的,并且通過(guò)使用其他類(lèi)型的作用域單元甚至可以實(shí)現(xiàn)維護(hù)起來(lái)更加優(yōu)秀凡涩、簡(jiǎn)潔的代碼棒搜。

for (var i = 0; i < 10; i++) {
            console.log(i);
        } 

我們?cè)?for 循環(huán)的頭部直接定義了變量 i,通常是因?yàn)橹幌朐?for 循環(huán)內(nèi)部的上下文中使 用 i活箕,而忽略了 i 會(huì)被綁定在外部作用域(函數(shù)或全局)中的事實(shí)力麸。

使用塊作用域可以幫助我們避免污染全局作用域。

var foo = true; 
    if (foo) {
        var bar = foo * 2;
         bar = something(bar);
        console.log(bar);
    }

bar 變量?jī)H在 if 聲明的上下文中使用育韩,以為我們用 {} 把它包裹在了一個(gè)塊當(dāng)中克蚂,這樣就避免了不污染全局作用域。

如果你還是不能理解塊級(jí)作用域的好處筋讨,希望下面這個(gè)例子可以幫助你理解:

for (let i = 0; i < 10; i++) {
    setTimeout(() => {
        console.log(i);
    }, 1000);
} 

正常情況下埃叭,我們對(duì)這段代碼行為的預(yù)期是分別輸出數(shù)字 0~9,每秒一次悉罕,每次一個(gè)赤屋。但實(shí)際上立镶,這段代碼在運(yùn)行時(shí)會(huì)以每秒一次的頻率輸出10次 10

這是為什么类早?

要理解 10 是怎么來(lái)的媚媒,我們需要理清以下兩點(diǎn):

  1. 由于一次又一次的循環(huán),循環(huán)結(jié)束時(shí)莺奔,i 此時(shí)已經(jīng)是 10 了欣范。換句話說(shuō),當(dāng) i 是 10 的時(shí)候令哟,循環(huán)結(jié)束。
  2. var 關(guān)鍵字聲明其實(shí)是一個(gè)全局聲明

這樣我們就很好理解這個(gè)結(jié)果了妨蛹。首先定時(shí)器會(huì)在 1s 中之后輸出屏富,此時(shí) i 已經(jīng)變成了 10,而 i 又是個(gè)全局變量蛙卤,所以此時(shí)輸出的會(huì)是 10 次 一樣的值狠半。

那我們?nèi)绾尾拍懿晃廴救肿饔糜蚰兀窟@里我推薦使用 let 關(guān)鍵字颤难,let 關(guān)鍵字可以將變量綁定到所在的任意作用域中(通常是 { .. } 內(nèi)部)神年。換句話說(shuō),let 為其聲明的變量隱式地了所在的塊作用域行嗤。(不了解 es6 中新增的 let 和 const 關(guān)鍵字可以先跳過(guò)這里往后看再回到這里)

如果我們用 let 替換 var 關(guān)鍵字已日,你會(huì)發(fā)現(xiàn)我們非常成功的獲取到了我們認(rèn)為的結(jié)果,原因是因?yàn)?let 關(guān)鍵字把變量 i 綁定在了我當(dāng)前這個(gè)作用域(循環(huán))中栅屏,因此全局作用域中并不會(huì)出現(xiàn) i 變量飘千,每次循化都會(huì)綁定一個(gè)獨(dú)立的值。

2. 垃圾收集

另一個(gè)塊作用域非常有用的原因和閉包及回收內(nèi)存垃圾的回收機(jī)制相關(guān)栈雳。在這里我們只看一個(gè)小小的例子护奈。

function process(data) {
        // 在這里做點(diǎn)有趣的事情 
    }

    // 在這個(gè)塊中定義的內(nèi)容可以銷(xiāo)毀了!   
    {
        let someReallyBigData = {..
        }

        process(someReallyBigData);
    }

    var btn = document.getElementById("my_button");

    btn.addEventListener("click", function click(evt) {
        console.log("button clicked");
    });

es6 新增的 let & const 關(guān)鍵字

let 關(guān)鍵字可以把變量綁定在當(dāng)前作用域中哥纫,外面作用域無(wú)法獲得這個(gè)變量霉旗。

for (let i = 0; i < 10; i++) {
    console.log(i); // 0 ~ 9
}
console.log(i); // Uncaught ReferenceError: i is not defined

const,同樣可以用來(lái)創(chuàng)建塊作用域變量蛀骇,但其值是固定的(常量)厌秒。之后任何試圖修改值的操作都會(huì)引起錯(cuò)誤。

var foo = true;

    if (foo) {
        var a = 2;
        const b = 3; // 包含在 if 中的塊作用域常量 

        a = 3; // 正常 !     

        b = 4; // 錯(cuò)誤 ! 
    }

    console.log(a); // 3 console.log( b ); // ReferenceError!

雖然說(shuō)我們?cè)诼暶髯兞康臅r(shí)候用 var 沒(méi)有什么問(wèn)題松靡,但是一旦我們作用域中變量多起來(lái)简僧,我們的大腦可能就會(huì)處于一種混亂的狀態(tài),出現(xiàn) bug 時(shí)會(huì)不知所措(找 bug 是真的頭痛哦)雕欺!在這里我還是推薦小伙伴們可以盡量使用 let 或 const 這種新增的關(guān)鍵字岛马。

結(jié)語(yǔ)

看到這里我們就已經(jīng)學(xué)習(xí)完了 JavaScript 中作用域棉姐,之所以我們要了解并掌握作用域,最大的原因是它能幫助我們快速的分析代碼出錯(cuò)之處啦逆,它對(duì)我們的影響也會(huì)潛移默化的體現(xiàn)在我們書(shū)寫(xiě)的代碼當(dāng)中伞矩,相信小伙伴們未來(lái)都能擺脫 bug 煩惱!:)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末夏志,一起剝皮案震驚了整個(gè)濱河市乃坤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌沟蔑,老刑警劉巖湿诊,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異瘦材,居然都是意外死亡厅须,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)食棕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)朗和,“玉大人,你說(shuō)我怎么就攤上這事簿晓】衾” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵憔儿,是天一觀的道長(zhǎng)忆植。 經(jīng)常有香客問(wèn)我,道長(zhǎng)皿曲,這世上最難降的妖魔是什么唱逢? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮屋休,結(jié)果婚禮上坞古,老公的妹妹穿的比我還像新娘。我一直安慰自己劫樟,他們只是感情好痪枫,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著叠艳,像睡著了一般奶陈。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上附较,一...
    開(kāi)封第一講書(shū)人閱讀 51,541評(píng)論 1 305
  • 那天吃粒,我揣著相機(jī)與錄音,去河邊找鬼拒课。 笑死徐勃,一個(gè)胖子當(dāng)著我的面吹牛集嵌,可吹牛的內(nèi)容都是我干的等曼。 我是一名探鬼主播,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼月而,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼柔袁!你這毒婦竟也來(lái)了美旧?” 一聲冷哼從身側(cè)響起铺峭,我...
    開(kāi)封第一講書(shū)人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤肤粱,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后揉稚,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體秒啦,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年窃植,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了帝蒿。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡巷怜,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出暴氏,到底是詐尸還是另有隱情延塑,我是刑警寧澤,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布答渔,位于F島的核電站关带,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏沼撕。R本人自食惡果不足惜宋雏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望务豺。 院中可真熱鬧磨总,春花似錦、人聲如沸笼沥。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)奔浅。三九已至馆纳,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間汹桦,已是汗流浹背鲁驶。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留舞骆,地道東北人钥弯。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓径荔,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親寿羞。 傳聞我的和親對(duì)象是個(gè)殘疾皇子猖凛,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355