JavaScript 作用域

作用域是什么

參考和摘錄自《你不知道的JavaScript(上)》

編譯原理

通常將 JavaScript 歸類為“動態(tài)”或“解釋執(zhí)行”語言寂纪,但事實上它是一門編譯語言栈拖。但與傳統(tǒng)的編譯語言不同绽族,它不是提前編譯的者疤,編譯結(jié)果也不能在分布式系統(tǒng)中進行移植苫纤。

【傳統(tǒng)編譯語言編譯步驟】:

  • 分詞/詞法分析(Tokenizing/Lexing):這個過程會將由字符組成的字符串分解成有意義(對編程語言來說)的代碼塊互订,這些代碼塊被稱為詞法單元(token)吱肌。例如,var a = 2;仰禽。這段程序通常會被分解為如下詞法單元:var氮墨、a、=吐葵、2规揪、;∥虑停空格是否會被當(dāng)作詞法單元猛铅,取決于空格在這門語言中是否具有意義。
  • 解析/語法分析(Parsing):將詞法單元流(數(shù)組)轉(zhuǎn)換成一個由元素逐級嵌套所組成的代表了程序語法結(jié)構(gòu)的樹凤藏。這個樹被稱為“抽象語法樹”(Abstract Syntax Tree奸忽,AST)。
  • 代碼生成:將 AST 轉(zhuǎn)換為可執(zhí)行代碼的過程被稱為代碼生成揖庄。這個過程與語言栗菜、目標(biāo)平臺等息息相關(guān)。拋開具體細(xì)節(jié)蹄梢,簡單來說就是有某種方法可以把 var a = 2; 的 AST 轉(zhuǎn)化為一組機器指令疙筹,用來創(chuàng)建一個叫作 a 的變量(包括分配內(nèi)存等),并將一個值儲存在 a 中。

比起那些編譯過程只有三個步驟的語言的編譯器腌歉,JavaScript 引擎要復(fù)雜得多蛙酪。例如,在語法分析和代碼生成階段有特定的步驟來對運行性能進行優(yōu)化翘盖,包括對冗余元素進行優(yōu)化等桂塞。

首先,JavaScript 引擎不會有大量的(像其他語言編譯器那么多的)時間用來進行優(yōu)化馍驯,因為與其他語言不同阁危,JavaScript 的編譯過程不是發(fā)生在構(gòu)建之前的。對于 JavaScript 來說汰瘫,大部分情況下編譯發(fā)生在代碼執(zhí)行前的幾微妙(甚至更短?翊颉)的時間內(nèi)。

簡單地說混弥,任何 JavaScript 代碼片段在執(zhí)行前都要進行編譯(通常就在執(zhí)行前)趴乡。因此,JavaScript 編譯器首先會對 var a = 2; 這段程序進行編譯蝗拿,然后做好執(zhí)行它的準(zhǔn)備晾捏,并且通常馬上就會執(zhí)行它。

理解作用域

學(xué)習(xí)作用域的方式是將這個過程模擬成幾個人物之間的對話哀托。

演員表

  • 引擎:從頭到尾負(fù)責(zé)整個 JavaScript 程序的編譯及執(zhí)行過程惦辛。
  • 編譯器:引擎的好朋友之一,負(fù)責(zé)語法分析及代碼生成等工作仓手。
  • 作用域:引擎的另一位好朋友胖齐,負(fù)責(zé)收集并維護由所有聲明的標(biāo)識符(變量)組成的一系列查詢,并實施一套非常嚴(yán)格的規(guī)則嗽冒,確定當(dāng)前執(zhí)行的代碼對這些標(biāo)識符的訪問權(quán)限呀伙。

對話(舉例)

在執(zhí)行 var a = 2; 這段代碼時,引擎會認(rèn)為這里有兩個完全不同的聲明辛慰,一個由編譯器在編譯時處理区匠,另一個則由引擎在運行時 處理。

編譯器首先會將這段程序分解成詞法單元帅腌,然后將詞法單元解析成一個樹結(jié)構(gòu)驰弄,最后開始進行代碼生成:

  1. 遇到 var a,編譯器會詢問作用域是否已經(jīng)有一個該名稱的變量存在于同一個作用域的集合中速客。如果是戚篙,編譯器會忽略該聲明,繼續(xù)進行編譯溺职;否則它會要求作用域在當(dāng)前作用域的集合中聲明一個新的變量岔擂,并命名為 a位喂。
  2. 接下來編譯器會為引擎生成運行時所需的代碼,這些代碼被用來處理 a = 2 這個賦值操作。引擎運行時會首先詢問作用域,在當(dāng)前的作用域集合中是否存在一個叫作 a 的變量侵俗。如果是,引擎就會使用這個變量规婆;如果否,引擎會繼續(xù)查找該變量蝉稳。如果最終找到了這個變量抒蚜,就會將 a 賦值給它。否則引擎就會拋出一個異常耘戚。

【總結(jié)】:變量的賦值操作會執(zhí)行兩個動作嗡髓,首先編譯器會在當(dāng)前作用域中聲明一個變量(如果之前沒有聲明過),然后在運行時引擎會在作用域中查找該變量收津,如果能夠找到就會對它賦值饿这。

編譯器有話說

引擎在查找變量的過程由左右能夠與進行協(xié)助,但是引擎執(zhí)行怎樣的查找撞秋,會影響最終的查找結(jié)果蛹稍。

【查找類型】:

  • LHS 查詢:賦值操作的目標(biāo)是誰?
  • RHS 查詢:誰是賦值操作的源頭部服?

【L 和 R 的含義】:分別代表變量出現(xiàn)的位置在賦值操作的左側(cè)和右側(cè)。也就是說拗慨,當(dāng)變量出現(xiàn)在賦值操作的左側(cè)時廓八,執(zhí)行 LHS 查詢。當(dāng)變量出現(xiàn)在賦值操作的右側(cè)時赵抢,執(zhí)行 RHS 查詢剧蹂。

【注意】:RHS 查詢與簡單地查找某個變量的值別無二致,而 LHS 查詢則是試圖找到變量的容器本身烦却,從而可以對其賦值宠叼。從這個角度說,RHS 并不是真正意義上的“賦值操作的右側(cè)”其爵,更準(zhǔn)確地說“非左側(cè)”冒冬。可以將其理解成 retrieve his source value(取到它的源值)摩渺,這意味著“得到某某的值”简烤。

【示例】:

console.log(a); // RHS 引用
a = 2; // LHS 引用

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

foo(2);

【解釋】:

  1. 執(zhí)行 foo() 函數(shù)的調(diào)用需要對 foo 進行 RHS 引用。
  2. 參數(shù)傳遞過程中的隱式分配摇幻,此時進行 LHS 查詢横侦。
  3. console.log() 本身也需要一個引用才能執(zhí)行挥萌,因此會對 console 對象進行 RHS 查詢,并且檢查得到的值中是否有一個叫作 log 的方法(RHS 查詢)
  4. console.log(a) 對 a 進行 RHS 引用枉侧。

【對話的形式來解釋】:

引擎:我說作用域引瀑,我需要為 foo 進行 RHS 引用。你見過它嗎榨馁?
作用域:別說憨栽,我還真見過,編譯器那小子剛剛聲明了它辆影。它是一個函數(shù)徒像,給你。
引擎:哥們太夠意思了蛙讥!好吧锯蛀,我來執(zhí)行一下 foo。
引擎:作用域次慢,還有個事兒旁涤。我需要為 a 進行 LHS 引用,這個你見過嗎迫像?
作用域:這個也見過劈愚,編譯器最近把它聲明為 foo 的一個形式參數(shù)了,拿去吧闻妓。
引擎:大恩不言謝菌羽,你總是這么棒。現(xiàn)在我要把 2 賦值給 a由缆。
引擎:哥們注祖,不好意思又來打擾你。我要為 console 進行 RHS 引用均唉,你見過它嗎是晨?
作用域:咱倆誰跟誰啊,再說我就是干這個舔箭。這個我也有罩缴,console 是個內(nèi)置對象。給你层扶。
引擎:么么噠箫章。我得看看這里面是不是有 log()。太好了怒医,找到了炉抒,是一個函數(shù)。
引擎:哥們稚叹,能幫我再找一下對 a 的 RHS 引用嗎焰薄?雖然我記得它拿诸,但想再確認(rèn)一次。
作用域:放心吧塞茅,這個變量沒有變動過亩码,拿走,不懈野瘦。
引擎:真棒描沟。我來把 a 的值,也就是 2鞭光,傳遞進 log()吏廉。
......

作用域嵌套

作用域是根據(jù)名稱查找變量的一套規(guī)則。實際情況下惰许,通常需要同時顧及幾個作用域席覆。

當(dāng)一個塊或函數(shù)嵌套在另一個塊或函數(shù)中時,就發(fā)生了作用域的嵌套汹买。因此佩伤,在當(dāng)前作用域中無法找到某個變量時,引擎就會在外層嵌套的作用域中繼續(xù)查找晦毙,直到找到該變量生巡,或抵達(dá)最外層的作用域(也就是全局作用域)為止。

function foo(a) {
    console.log(a + b);
}

var b = 2;

foo(2); // 4

對 b 進行的 RHS 引用無法在函數(shù) foo 內(nèi)部完成见妒,但可以在上一級作用域(在上面例子中是在全局作用域中)中完成孤荣。

【小劇場】:

引擎:foo 的作用域兄弟,你見過 b 嗎须揣?我需要對它進行 RHS 引用垃环。
作用域:聽都沒聽過,走開返敬。
引擎:foo 的上級作用域兄弟,咦寥院?有眼不識泰山劲赠,原來你是全局作用域大哥,太好了秸谢。你見過 b 嗎凛澎?我需要對它進行 RHS 引用。
作用域:當(dāng)然了估蹄,給你吧塑煎。

遍歷嵌套作用域規(guī)則:引擎從當(dāng)前的執(zhí)行作用域開始查找變量。如果找不到臭蚁,就向上一級繼續(xù)查找最铁。當(dāng)?shù)诌_(dá)最外層的全局作用域時讯赏,無論找到還是沒找到,查找過程都會停止冷尉。

異常

【問】:為什么區(qū)分 LHS 和 RHS 是一件重要的事情漱挎?

【答】:因為在變量還沒有聲明(在任何作用域中都無法找到該變量)的情況下,這兩種查詢的行為是不一樣的雀哨。

舉例說明

function foo(a) {
    console.log(a + b);
    b = a;
}

foo(2);

第一次對 b 進行 RHS 查詢時是無法找到該變量的磕谅。也就是說,這是一個“未聲明”的變量雾棺,因為在任何相關(guān)的作用域中都無法找到它膊夹。

如果 RHS 查詢在所有嵌套的作用域中遍尋不到所需的變量,引擎就會拋出 ReferenceError 異常捌浩。

【注意】:ReferenceError 是非常重要的異常類型放刨。

相較之下,當(dāng)引擎執(zhí)行 LHS 查詢時嘉栓,如果在頂層(全局作用域)中也無法找到目標(biāo)變量宏榕,全局作用域中就會創(chuàng)建一個具有該名稱的變量,并將其返還給引擎侵佃,前提是程序運行在非“嚴(yán)格模式”下麻昼。

作用域:“不,這個變量之前并不存在馋辈,但是我很熱心地幫你創(chuàng)建了一個抚芦。”

ES5 中引入了“嚴(yán)格模式”迈螟。同正常模式叉抡,或者說寬松、懶惰模式答毫,嚴(yán)格模式在行為上有很多不同褥民。其中一個不同的行為是嚴(yán)格模式禁止自動或隱式地創(chuàng)建全局變量。因此洗搂,在嚴(yán)格模式中 LHS 查詢失敗時消返,并不會創(chuàng)建并返回一個全局變量,引擎會拋出同 RHS 查詢失敗時類似的 ReferenceError 異常耘拇。

接下來撵颊,如果 RHS 查詢找到了一個變量,但是你嘗試對這個變量的值進行不合理的操作惫叛,比如試圖對一個非函數(shù)類型的值進行函數(shù)調(diào)用倡勇,或者引用 null 或 undefined 類型的值中的屬性,那么引擎會拋出另外一種類型的異常嘉涌,叫作 TypeError妻熊。

ReferenceError 同作用域判別失敗相關(guān)夸浅,而 TypeError 則代表作用域判別成功了,但是對結(jié)果的操作是非法或不合理的固耘。

小結(jié)

  • 作用域是一套規(guī)則题篷,用于確定在何處以及如何查找變量(標(biāo)識符)。如果查找的目的是對變量進行賦值厅目,那么就會使用 LHS 查詢番枚;如果目的是獲取變量的值,就會使用 RHS 查詢损敷。賦值操作符會導(dǎo)致 LHS 查詢葫笼。= 操作符或調(diào)用函數(shù)時傳入?yún)?shù)的操作都會導(dǎo)致關(guān)聯(lián)作用域的賦值操作。
  • JavaScript 引擎首先會在代碼執(zhí)行前對其進行編譯拗馒,在這個過程中路星,像 var a = 2; 這樣的聲明會被分解成兩個獨立的步驟:
    1. 首先,var a 在其作用域中聲明新變量诱桂。這會在最開始的階段洋丐,也就是代碼執(zhí)行前進行。
    2. 接下來挥等,a = 2 會查詢(LHS 查詢)變量 a 并對其進行賦值友绝。
  • LHS 和 RHS 查詢都會在當(dāng)前執(zhí)行作用域中開始,如果有需要(也就是說它們沒有找到所需的標(biāo)識符)肝劲,就會向上級作用域繼續(xù)查找目標(biāo)標(biāo)識符迁客,這樣每次上升一級作用域,最后抵達(dá)全局作用域辞槐,無論找到或沒找到都將停止掷漱。
  • 不成功的 RHS 引用會導(dǎo)致拋出 ReferenceError 異常。不成功的 LHS 引用會導(dǎo)致自動隱式地創(chuàng)建一個全局變量(非嚴(yán)格模式下)榄檬,該變量使用 LHS 引用的目標(biāo)作為標(biāo)識符卜范,或者拋出 ReferenceError 異常(嚴(yán)格模式下)。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鹿榜,一起剝皮案震驚了整個濱河市先朦,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌犬缨,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件棉浸,死亡現(xiàn)場離奇詭異怀薛,居然都是意外死亡,警方通過查閱死者的電腦和手機迷郑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進店門枝恋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來创倔,“玉大人,你說我怎么就攤上這事焚碌∑枞粒” “怎么了?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵十电,是天一觀的道長知押。 經(jīng)常有香客問我,道長鹃骂,這世上最難降的妖魔是什么台盯? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮畏线,結(jié)果婚禮上静盅,老公的妹妹穿的比我還像新娘。我一直安慰自己寝殴,他們只是感情好蒿叠,可當(dāng)我...
    茶點故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蚣常,像睡著了一般市咽。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上史隆,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天魂务,我揣著相機與錄音,去河邊找鬼泌射。 笑死粘姜,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的熔酷。 我是一名探鬼主播孤紧,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼拒秘!你這毒婦竟也來了号显?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤躺酒,失蹤者是張志新(化名)和其女友劉穎押蚤,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體羹应,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡揽碘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片雳刺。...
    茶點故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡劫灶,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出掖桦,到底是詐尸還是另有隱情本昏,我是刑警寧澤,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布枪汪,位于F島的核電站涌穆,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏料饥。R本人自食惡果不足惜蒲犬,卻給世界環(huán)境...
    茶點故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望岸啡。 院中可真熱鬧原叮,春花似錦、人聲如沸巡蘸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽悦荒。三九已至唯欣,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間搬味,已是汗流浹背境氢。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留碰纬,地道東北人萍聊。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像悦析,于是被迫代替她去往敵國和親寿桨。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,691評論 2 361

推薦閱讀更多精彩內(nèi)容

  • 如果我的文章對你有用,請給我一個贊骑歹,讓我有繼續(xù)堅持的動力/微笑预烙。原創(chuàng)文章,此文章僅供學(xué)習(xí)參考使用道媚,歡迎訪問我的個人...
    我就是z閱讀 489評論 0 3
  • 本文首發(fā)于貝殼社區(qū)FE專欄扁掸,歡迎關(guān)注欢嘿! 一、什么是作用域 編譯原理 分詞/詞法分析(Tokenizing/Lexi...
    VioletJack閱讀 385評論 0 3
  • 作用域是一套用于確定在何處以及如何查找變量的規(guī)則也糊。賦值操作會導(dǎo)致LHS查詢,獲取變量的值得操作會導(dǎo)致RHS查詢羡宙。引...
    Adambee08閱讀 304評論 0 0
  • LHS & RHS 編譯器在遇到一個變量 a 時狸剃,會去查詢作用域中是否存在 a 。但是有兩種不同查詢方式狗热,考慮如下...
    小小小超子閱讀 140評論 0 0
  • 什么是作用域钞馁? 作用域相當(dāng)于一套設(shè)計良好的用于存儲變量并易于訪問這些變量的規(guī)則。作用域根據(jù)確定的周期不同分為靜態(tài)作...
    _敏訥閱讀 140評論 0 0