作用域和閉包
1叠纷、作用域
編譯原理:
總所周知,JavaScript是一門解釋型語言悔耘,但事實上它是一門編譯語言。
傳統(tǒng)編譯語言編譯的步驟:
- 分詞/詞法分析(Tokenizing/Lexing)
將由字符組成的字符串分解成( 對編程語言來說)有意義的代碼塊我擂, 這些代碼塊被稱為詞法單元(token)衬以。
var a = 2;
// 分解成 var缓艳、a、=看峻、2阶淘、;
- 解析/語法分析(Parsing)
這個過程是將詞法單元流( 數(shù)組)轉(zhuǎn)換成一個由元素逐級嵌套所組成的代表了程序語法結(jié)構(gòu)的樹。這個樹被稱為“抽象語法樹”(Abstract Syntax Tree互妓,AST)溪窒。
var a = 2;的抽象語法樹中可能會有一個叫作VariableDeclaration的頂級節(jié)點,接下來是一個叫作Identifier(它的值是a)的子節(jié)點冯勉,以及一個叫作AssignmentExpression的子節(jié)點澈蚌。AssignmentExpression節(jié)點有一個叫作NumericLiteral(它的值是2)的子節(jié)點。
- 代碼生成
將AST轉(zhuǎn)換為可執(zhí)行代碼的過程稱被稱為代碼生成灼狰。 這個過程與語言宛瞄、 目標(biāo)平臺等息息相關(guān)。拋開具體細(xì)節(jié)交胚,簡單來說就是有某種方法可以將var a = 2;的AST轉(zhuǎn)化為一組機器指令份汗,用來創(chuàng)建一個叫作a的變量(包括分配內(nèi)存等),并將一個值儲存在a中蝴簇。
JavaScript中的編譯
- 首先杯活,JavaScript引擎不會有大量的(像其他語言編譯器那么多的)時間用來進(jìn)行優(yōu)化,因為與其他語言不同熬词,JavaScript的編譯過程不是發(fā)生在構(gòu)建之前的旁钧。
- 對于JavaScript來說, 大部分情況下編譯發(fā)生在代碼執(zhí)行前的幾微秒( 甚至更短5磁臁) 的時間內(nèi)均践。
- 簡單地說, 任何JavaScript代碼片段在執(zhí)行前都要進(jìn)行編譯( 通常就在執(zhí)行前)摩幔。因此彤委,JavaScript編譯器首先會對var a = 2;這段程序進(jìn)行編譯, 然后做好執(zhí)行它的準(zhǔn)備或衡, 并且通常馬上就會執(zhí)行它焦影。
總而言之,JavaScript不會像Java那樣封断,先進(jìn)行編譯斯辰,再把編譯過后的文件拿去運行,而是直接去運行寫好的文件坡疼,但在運行的時候會作一次內(nèi)部的編譯彬呻,再逐行執(zhí)行(那么報錯到底是發(fā)生在預(yù)編譯還是代碼執(zhí)行?)。
解析作用域
三個角色:
- 引擎:從頭到尾負(fù)責(zé)整個JavaScript程序的編譯及執(zhí)行過程闸氮。
- 編譯器:引擎的好朋友之一剪况,負(fù)責(zé)語法分析及代碼生成等臟活累活。
- 作用域:引擎的另一位好朋友蒲跨, 負(fù)責(zé)收集并維護(hù)由所有聲明的標(biāo)識符( 變量)組成的一系列查詢译断,并實施一套非常嚴(yán)格的規(guī)則,確定當(dāng)前執(zhí)行的代碼對這些標(biāo)識符的訪問權(quán)限或悲。
對于 var a = 2 的處理
遇到var a孙咪,編譯器會詢問作用域是否已經(jīng)有一個該名稱的變量存在于同一個作用域的集合中。 如果是巡语, 編譯器會忽略該聲明翎蹈, 繼續(xù)進(jìn)行編譯; 否則它會要求作用域在當(dāng)前作用域的集合中聲明一個新的變量捌臊,并命名為a杨蛋。
接下來編譯器會為引擎生成運行時所需的代碼,這些代碼被用來處理a = 2這個賦值操作理澎。引擎運行時會首先詢問作用域逞力,在當(dāng)前的作用域集合中是否存在一個叫作a的變量。如果是糠爬,引擎就會使用這個變量寇荧;如果否,引擎會繼續(xù)查找該變量执隧。
LHS查詢與RHS查詢
當(dāng)變量出現(xiàn)在賦值操作的左側(cè)時進(jìn)行LHS查詢揩抡,出現(xiàn)在右側(cè)時進(jìn)行RHS查詢。
RHS查詢與簡單地查找某個變量的值別無二致镀琉, 而LHS查詢則是試圖找到變量的容器本身峦嗤, 從而可以對其賦值。
var a
a = 2 // LHS查詢
console.log(a) // RHS查詢
作用域嵌套查詢:
當(dāng)一個塊或函數(shù)嵌套在另一個塊或函數(shù)中時屋摔, 就發(fā)生了作用域的嵌套烁设。 因此, 在當(dāng)前作用域中無法找到某個變量時钓试, 引擎就會在外層嵌套的作用域中繼續(xù)查找装黑, 直到找到該變量,或抵達(dá)最外層的作用域(也就是全局作用域)為止弓熏。
當(dāng)?shù)诌_(dá)最外層的全局作用域時恋谭, 無論找到還是沒找到, 查找過程都會停止挽鞠。
LHS查詢和RHS查詢未找到預(yù)期結(jié)果的時候
LHS查詢
function foo() {
b = 3
return b
}
foo()
/**
* 在執(zhí)行 b = 2 語句的時候疚颊,引擎首先會通過一次LHS查詢狈孔,試圖查詢 b 標(biāo)識符
* 在 foo 內(nèi)部沒有找到,然后再去找上一層作用域(也就是全局作用域)材义,也沒有找到
* 此時全局作用域就會創(chuàng)建一個變量除抛,接著引擎順利執(zhí)行了后面的賦值操作(非嚴(yán)格模式下)
* 這也是為什么直接給一個變量賦值,會在全局作用域聲明一個同名變量并賦值
*/
RHS查詢
function bar() {
return b
}
bar()
/**
* 引擎通過RHS查詢首先找尋變量b
* 在本層作用域(bar內(nèi)部)沒有找到母截,會繼續(xù)向外層作用域
* 上一層全局作用域,依然沒有找到橄教,這時候作用域就會拋出一個錯誤
* ReferenceError: b is not defined
*/
2清寇、詞法作用域
詞法作用域就是定義在詞法階段的作用域。 換句話說护蝶, 詞法作用域是由你在寫代碼時將變量和塊作用域?qū)懺谀睦飦頉Q定的华烟, 因此當(dāng)詞法分析器處理代碼時會保持作用域不變(大部分情況下是這樣的)。
作用域嵌套
function foo(a) {
var b = a + 1
function bar(c) {
console.log(a, b, c)
}
bar(b * 3)
}
foo(2)
/**
* 全局作用域 {foo: func}
* foo作用域 {a:, b:, bar:}
* bar作用域 {c: ,}
*/
全局變量會自動成為全局對象持灰,可通過 window.xxx 訪問盔夜。
兩種欺騙語法:
- eval:eval是魔鬼
JavaScript中的eval(..)函數(shù)可以接受一個字符串為參數(shù), 并將其中的內(nèi)容視為好像在書寫時就存在于程序中這個位置的代碼堤魁。
function foo(str) {
eval(str)
console.log(a)
}
var a = 1
var evalStr = 'var a = 2'
foo(evalStr) // 2
這段代碼會被當(dāng)作本來就在那里一樣來處理喂链。這段代碼foo(..)的詞法作用域進(jìn)行了修改,聲明了一個新的變量a妥泉。
- with:
var obj = {
name: 'normal',
age: 5,
}
with(obj) {
name = 'with'
type = 'with'
}
/**
* with() {}
* () 里面表示 {} 里面寫的代碼的作用域
*/
console.log(obj) // { name: 'with', age: 5 }
eval(..)和with會在運行時修改或創(chuàng)建新的作用域椭微, 以此來欺騙其他在書寫時定義的詞法作用域∶ち矗總而言之蝇率,不要使用它們。
3刽沾、函數(shù)作用域和塊作用域
函數(shù)作用域的含義是指本慕, 屬于這個函數(shù)的全部變量都可以在整個函數(shù)的范圍內(nèi)使用及復(fù)用( 事實上在嵌套的作用域中也可以使用)。
使用函數(shù)作用域的好處:
- 使用函數(shù)作用域把變量包裝起來侧漓,可以避免污染全局變量
- 可以實現(xiàn)模塊化管理锅尘,向外暴露盡量少的東西。
函數(shù)聲明和函數(shù)表達(dá)式
/**
* es6函數(shù)新增了name屬性
* 匿名函數(shù)在棧追蹤中不會顯示出有意義的函數(shù)名火架,使得調(diào)試很困難鉴象。
*/
function foo() {}
foo.name // 'foo'
// 函數(shù)表達(dá)式可以是匿名的
var bar = function () {}
bar.name // 'bar'
// 函數(shù)表達(dá)式也可以是具名的
var foo = function bar() {}
foo.name // 'bar'
立即執(zhí)行函數(shù)
兩種形式:
- (function () {}())
- (function () {})()
塊級作用域
一個容易忽視的點:
for (var i = 0; i < 5; i ++) {
console.log(i) // 0 1 2 3 4
}
// 這里的 i 在全局也能訪問
console.log(i) // 5 當(dāng) i 等于 5 的時候,就不符合 i < 5何鸡,就不會走后面的 i++
我們在for循環(huán)的頭部直接定義了變量i纺弊,通常是因為只想在for循環(huán)內(nèi)部的上下文中使用i,而忽略了i會被綁定在外部作用域(函數(shù)或全局)中的事實骡男。
try/catch
JavaScript的ES3規(guī)范中規(guī)定try/catch的catch分句會創(chuàng)建一個塊作用域淆游,其中聲明的變量僅在catch內(nèi)部有效。
try {
console.log(a)
} catch (err) {
console.log(err) // 程序不會報錯,打印出出錯信息 a is not defined
}
console.log(err) // 程序出錯犹菱,a is not defined
let 聲明
let關(guān)鍵字可以將變量綁定到所在的任意作用域中(通常是{ .. }內(nèi)部) 拾稳。換句話說,let為其聲明的變量隱式地了所在的塊作用域腊脱。
for(let i = 0; i < 5; i ++) {
}
console.log(i) // 報錯