深入解析JavaScript中的作用域(轉(zhuǎn)載)

本篇文章帶大家深入理解JavaScript作用域像啼。有一定的參考價(jià)值喻括,有需要的朋友可以參考一下,希望對大家有所幫助驰徊。

image

這篇文章稱為筆記更為合適一些,內(nèi)容來源于 《你不知道的JavaScript(上卷)》第一部分 作用域和閉包堕阔。講的很不錯(cuò)棍厂,非常值得一看。

什么是作用域

作用域是根據(jù)名稱查找變量的一套規(guī)則超陆。

理解作用域

先來理解一些基礎(chǔ)概念:

  • 引擎:從頭到尾負(fù)責(zé)整個(gè)JavaScript程序的編譯及執(zhí)行過程牺弹。
  • 編譯器:負(fù)責(zé)語法分析和代碼生成。這部分也可以看 JavaScript代碼是如何被執(zhí)行的
  • 作用域:負(fù)責(zé)收集并維護(hù)由所有聲明的標(biāo)識符(變量)組成的一系列查詢时呀,并實(shí)施一套非常嚴(yán)格的規(guī)則张漂,確定當(dāng)前執(zhí)行的代碼對這些標(biāo)識符的訪問權(quán)限。

接下來來看看下面代碼的執(zhí)行過程:

var a = 2;
  • 遇見 var a谨娜,編譯器 會問 作用域 變量a是否存在于同一個(gè)作用域集合中航攒。如果存在,編譯器會忽略聲明趴梢,繼續(xù)編譯漠畜;否則,會要求作用域在當(dāng)前作用域集合中聲明一個(gè)新的變量坞靶,并命名為 a

  • 接下來 編譯器 會為 引擎 生成運(yùn)行時(shí)所需的代碼憔狞,用來處理 a = 2 這個(gè)賦值操作。引擎運(yùn)行時(shí)會先問作用域彰阴,當(dāng)前作用域集中是否存在變量a瘾敢。如果是,引擎就會使用該變量硝枉;如果不存在廉丽,引擎會繼續(xù)查找該變量

  • 如果 引擎 找到了a 變量,就會將 2 賦值給它妻味,否則引擎就拋出一個(gè)錯(cuò)誤正压。

總結(jié):變量的賦值操作會執(zhí)行兩個(gè)動(dòng)作,首先編譯器會在當(dāng)前作用域中聲明一個(gè)變量责球,然后在運(yùn)行時(shí)引擎就會會作用域中查找該變量焦履,如果能夠找到就對它賦值。

編譯器在編譯過程的第二步中生成了代碼雏逾,引擎執(zhí)行它時(shí)嘉裤,會通過查找變量 a來判斷它是否已聲明過。查找的過程中由作用域進(jìn)行協(xié)助栖博,但是引擎執(zhí)行怎么樣的查找屑宠,會影響最終的查找結(jié)果。

在我們的例子中仇让,引擎會為變量 a 進(jìn)行 LHS 查詢典奉,另外一個(gè)查找的類型叫做 RHS躺翻。 ”L“ 和 "R" 分別代表一個(gè)賦值操作左側(cè)和右側(cè)。當(dāng)變量出現(xiàn)在賦值操作的左側(cè)時(shí)進(jìn)行 LHS 查詢卫玖,出現(xiàn)在右側(cè)時(shí)進(jìn)行 RHS 查詢公你。

LHS:試圖找到變量的容器本身,從而可以對其賦值假瞬;RHS: 就是簡單地查找某個(gè)變量的值陕靠。

console.log(a);

對 a 的引用是一個(gè) RHS 引用,因?yàn)檫@里 a 并沒有賦予任務(wù)值脱茉,相應(yīng)地需要查找并取得 a 的值剪芥,這樣才能將值傳遞給 console.log(...)

a = 2;

這里對 a 的引用是 LHS 引用,因?yàn)閷?shí)際上我們并不關(guān)心當(dāng)前的值是什么琴许,只是想要為 = 2這個(gè)賦值操作找到目標(biāo)粗俱。

funciton foo(a) {
    console.log(a)
}

foo(2);
  1. 最后一行 foo 函數(shù)的調(diào)用需要對 foo 進(jìn)行 RHS 引用,去找 foo的值虚吟,并把它給我
  2. 代碼中隱式的 a = 2 操作可能很容易被你忽略掉,這操作發(fā)生在 2 被當(dāng)做參數(shù)傳遞給 foo 函數(shù)時(shí)签财,2 會被分配給參數(shù) a串慰,為了給參數(shù) a (隱式地) 分配值,需要進(jìn)行一次 LHS 查詢唱蒸。
  3. 這里還有對 a 進(jìn)行的 RHS 引用邦鲫,并且將得到的值傳給了 console.log(...)console.log(...) 本身也需要一個(gè)引用才能執(zhí)行神汹,因此會對 console對象進(jìn)行 RHS 查詢庆捺,并且檢查得到的值中是否有一個(gè)叫做 log的方法。

RHS查詢在所有嵌套的作用域中遍尋不到所需的變量屁魏,引擎就會拋出 ReferenceError 異常滔以。進(jìn)行RHS查詢找到了一個(gè)變量,但是你嘗試對這個(gè)變量的值進(jìn)行不合理的操作氓拼,比如試圖對一個(gè)非函數(shù)類型的值進(jìn)行調(diào)用你画,后者引用null或 undefined 類型的值中的屬性,那么引擎會拋出一個(gè)另外一種類型的異常 TypeError桃漾。
引擎執(zhí)行 LHS 查詢時(shí)如果找不到該變量坏匪,則會在全局作用域中創(chuàng)建一個(gè)。但是在嚴(yán)格模式下撬统,并不是自動(dòng)創(chuàng)建一個(gè)全局變量适滓,而是會拋出 ReferenceError 異常

補(bǔ)充JS幾種常見的錯(cuò)誤類型

簡單總結(jié)如下:

作用域是一套規(guī)則,用于確定在哪里找恋追,怎么找到某個(gè)變量凭迹。如果查找的目的是對變量進(jìn)行賦值罚屋,那么就會使用 LHS查詢; 如果目的是獲取變量的值,就會使用 RHS 查詢蕊苗;
JavaScript 引擎執(zhí)行代碼前會對其進(jìn)行編譯沿后,這個(gè)過程中,像 var a = 2 這樣的聲明會被分解成兩個(gè)獨(dú)立的步驟

  • var a 在其作用域中聲明變量朽砰,這會在最開始的階段尖滚,也就是代碼執(zhí)行前進(jìn)行

  • 接下來,a = 2 會查詢 (LHS查詢)變量 a 并對其進(jìn)行賦值瞧柔。

詞法作用域

詞法作用域是你在寫代碼時(shí)將變量寫在哪里來決定的漆弄。編譯的詞法分析階段基本能夠知道全局標(biāo)識符在哪里以及是如何聲明的,從而能夠預(yù)測在執(zhí)行過程中如果對他們查找造锅。

有一些方法可以欺騙詞法作用域撼唾,比如 eval, with, 這兩種現(xiàn)在被禁止使用,1是嚴(yán)格模式和非嚴(yán)格模式下表現(xiàn)不同 2是有性能問題哥蔚, JavaScript引擎在編譯階段會做很多性能優(yōu)化倒谷,而其中很多優(yōu)化手段都依賴于能夠根據(jù)代碼的詞法進(jìn)行靜態(tài)分析,并預(yù)先確定所有變量和函數(shù)的定義位置糙箍,才能在執(zhí)行過程中快速找到識別符渤愁,eval, with會改變作用域,所以碰到它們深夯,引擎將無法做優(yōu)化處理抖格。

全局作用域和函數(shù)作用域

全局作用域

  • 在最外層函數(shù)和最外層函數(shù)外面定義的變量擁有全局作用域
var a = 1;
function foo() {

}

變量a 和函數(shù)聲明 foo 都是在全局作用域中的。

  • 所有未定義直接賦值的變量自動(dòng)聲明為擁有全局作用域
var a = 1;
function foo() {
    b = 2;
}
foo();
console.log(b); // 2
  • 所有 window 對象的屬性擁有全局作用域

函數(shù)作用域

函數(shù)作用域是指在函數(shù)內(nèi)聲明的所有變量在函數(shù)體內(nèi)始終是可見的咕晋。外部作用域無法訪問函數(shù)內(nèi)部的任何內(nèi)容雹拄。

function foo() {
    var a = 1;
    console.log(a); // 1
}
foo();
console.log(a); // ReferenceError: a is not defined

只有函數(shù)的{}構(gòu)成作用域,對象的{}以及 if(){}都不構(gòu)成作用域;

變量提升

提升是指聲明會被視為存在與其所出現(xiàn)的作用域的整個(gè)范圍內(nèi)掌呜。

JavaScript編譯階段是找到找到所有聲明滓玖,并用合適的作用域?qū)⑺麄冴P(guān)聯(lián)起來(詞法作用域核心內(nèi)容),所以就是包含變量和函數(shù)在內(nèi)的所有聲明都會在任何代碼被執(zhí)行前首先被處理质蕉。

每個(gè)作用域都會進(jìn)行提升操作呢撞。

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

注意,函數(shù)聲明會被提升饰剥,但是函數(shù)表達(dá)式不會被提升殊霞。

關(guān)于 塊級作用域和變量提升的內(nèi)容之前在 從JS底層理解var、let汰蓉、const這邊文章中詳細(xì)介紹過绷蹲,這里不再贅述。

塊級作用域

我們來看下面這段代碼

for(var i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i);
    })
}
console.log(`當(dāng)前的i為${i}`); // 當(dāng)前的i為5

上面這段代碼我們希望是輸出 0,1祝钢, 2比规, 3, 4 拦英,但是實(shí)際上輸出的是 5蜒什,5, 5, 5, 5。我們在 for 循環(huán)的頭部直接定義了變量 i疤估,通常是因?yàn)橹幌朐?for 循環(huán)內(nèi)部的上下文中使用 i灾常,但是實(shí)際上 此時(shí)的 i 被綁定在外部作用域(函數(shù)或全局)中。

铃拇,塊級作用域是指在指定的塊級作用域外無法訪問钞瀑。在ES6之前是沒有塊級作用域的概念的,ES6引入了 let 和 const慷荔。我們可以改寫上面的代碼雕什,使它按照我們想要的方式運(yùn)行。

for(let i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i);
    })
}
// 0 1 2 3 4
console.log(`當(dāng)前的i為${i}`); // ReferenceError: i is not defined

此時(shí) for 循環(huán)頭部的 let 不僅將 i 綁定到了 for 循環(huán)的迭代中显晶,事實(shí)上將它重新綁定到了循環(huán)的每一個(gè)迭代中贷岸,確保使用上一次循環(huán)迭代結(jié)束的值重新進(jìn)行賦值。

let聲明附屬于一個(gè)新的作用域而不是當(dāng)前的函數(shù)作用域(也不屬于全局作用域)磷雇。但是其行為是一樣的凰盔,可以總結(jié)為:任何聲明在某個(gè)作用域內(nèi)的變量,都將附屬于這個(gè)作用域倦春。
const也是可以用來創(chuàng)建塊級作用域變量,但是創(chuàng)建的是固定值落剪。

作用域鏈

JavaScript是基于詞法作用域的語言睁本,通過變量定義的位置就能知道變量的作用域。全局變量在程序中始終都有都定義的忠怖。局部變量在聲明它的函數(shù)體內(nèi)以及其所嵌套的函數(shù)內(nèi)始終是有定義的呢堰。

每一段 JavaScript 代碼都有一個(gè)與之關(guān)聯(lián)的作用域鏈(scope chain)。這個(gè)作用域鏈?zhǔn)且粋€(gè)對象列表或者鏈表凡泣。當(dāng) JavaScript 需要查找變量 x 的時(shí)候(這個(gè)過程稱為變量解析)枉疼,它會從鏈中的第一個(gè)變量開始查找,如果這個(gè)對象上依然沒有一個(gè)名為 x 的屬性鞋拟,則會繼續(xù)查找鏈上的下一個(gè)對象骂维,如果第二個(gè)對象依然沒有名為 x 的屬性,javaScript會繼續(xù)查找下一個(gè)對象贺纲,以此類推航闺。如果作用域鏈上沒有任何一個(gè)對象包含屬性 x, 那么就認(rèn)為這段代碼的作用域鏈上不存在 x, 并最終拋出一個(gè)引用錯(cuò)誤 (Reference Error) 異常潦刃。

下面作用域中有三個(gè)嵌套的作用域侮措。

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

![1.png](https://upload-images.jianshu.io/upload_images/19871305-ca3dc326980ace26.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240 "1622453204885970.png")

氣泡1包含著整個(gè)全局作用域,其中只有一個(gè)標(biāo)識符:foo乖杠;
氣泡2包含著foo所創(chuàng)建的作用域分扎,其中有三個(gè)標(biāo)識符:a、bar 和 b胧洒;
氣泡3包含著 bar所創(chuàng)建的作用域畏吓,其中只有一個(gè)標(biāo)識符:c

執(zhí)行 console.log(...),并查找 a略荡,b庵佣,c三個(gè)變量的引用。下面我們來看看查找這幾個(gè)變量的過程.
它首先從最內(nèi)部的作用域汛兜,也就是 bar(..) 函數(shù)的作用域氣泡開始找巴粪,引擎在這里無法找到 a,因此就會去上一級到所嵌套的 foo(...)的作用域中繼續(xù)查找粥谬。在這里找到了a肛根,因此就使用了這個(gè)引用。對b來說也一樣漏策,而對 c 來說派哲,引擎在 bar(..) 中就找到了它。

如果 a掺喻,c都存在于 bar(...) 內(nèi)部芭届,console.log(...)就可以直接使用 bar(...) 中的變量,而無需到外面的 foo(..)中查找感耙。作用域會在查找都第一個(gè)匹配的標(biāo)識符時(shí)就停止褂乍。

在多層的嵌套作用域中可以定義同名的標(biāo)識符,這叫”遮蔽效應(yīng)“即硼。

 var a = '外部的a';
function foo() {
    var a = 'foo內(nèi)部的a';
    console.log(a); // foo內(nèi)部的a
}
foo();

作用域與執(zhí)行上下文

JavaScript的執(zhí)行分為:解釋和執(zhí)行兩個(gè)階段

解釋階段

  • 詞法分析
  • 語法分析
  • 作用域規(guī)則確定

執(zhí)行階段

  • 創(chuàng)建執(zhí)行上下文
  • 執(zhí)行函數(shù)代碼
  • 垃圾回收

作用域在函數(shù)定義時(shí)就已經(jīng)確定了逃片,而不是在函數(shù)調(diào)用時(shí)確定,但執(zhí)行上下文是函數(shù)執(zhí)行之前創(chuàng)建的只酥。

總結(jié)

  • 作用域就是一套規(guī)則褥实,用于確定在哪里找以及怎么找到某個(gè)變量。

  • 詞法作用域在你寫代碼的時(shí)候就確定了裂允。JavaScript是基于詞法作用域的語言损离,通過變量定義的位置就能知道變量的作用域。ES6引入的let和const聲明的變量在塊級作用域中绝编。

  • 聲明提升是指聲明會被視為存在與其所出現(xiàn)的作用域的整個(gè)范圍內(nèi)草冈。

  • 查找變量的時(shí)候會先從內(nèi)部的作用域開始查找,如果沒找到,就往上一級進(jìn)行查找怎棱,依次類推哩俭。

  • 作用域在函數(shù)定義時(shí)就已經(jīng)確定了,執(zhí)行上下文是函數(shù)執(zhí)行之前創(chuàng)建的拳恋。

原文地址:https://www.php.cn/js-tutorial-477150.html

更多編程相關(guān)知識凡资,請?jiān)L問:編程視頻!谬运!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末隙赁,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子梆暖,更是在濱河造成了極大的恐慌伞访,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件轰驳,死亡現(xiàn)場離奇詭異厚掷,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)级解,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進(jìn)店門冒黑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人勤哗,你說我怎么就攤上這事抡爹。” “怎么了芒划?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵冬竟,是天一觀的道長。 經(jīng)常有香客問我民逼,道長泵殴,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任缴挖,我火速辦了婚禮,結(jié)果婚禮上焚辅,老公的妹妹穿的比我還像新娘映屋。我一直安慰自己,他們只是感情好同蜻,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布棚点。 她就那樣靜靜地躺著,像睡著了一般湾蔓。 火紅的嫁衣襯著肌膚如雪瘫析。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天,我揣著相機(jī)與錄音贬循,去河邊找鬼咸包。 笑死,一個(gè)胖子當(dāng)著我的面吹牛杖虾,可吹牛的內(nèi)容都是我干的烂瘫。 我是一名探鬼主播,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼奇适,長吁一口氣:“原來是場噩夢啊……” “哼坟比!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起嚷往,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤葛账,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后皮仁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體籍琳,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年魂贬,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了巩割。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,117評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡付燥,死狀恐怖宣谈,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情键科,我是刑警寧澤闻丑,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站勋颖,受9級特大地震影響嗦嗡,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜饭玲,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一侥祭、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧茄厘,春花似錦矮冬、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至窑滞,卻和暖如春琼牧,著一層夾襖步出監(jiān)牢的瞬間恢筝,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工巨坊, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留撬槽,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓抱究,卻偏偏與公主長得像恢氯,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子鼓寺,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評論 2 355

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