LHS & RHS
編譯器在遇到一個(gè)變量
a
時(shí),會(huì)去查詢作用域中是否存在a
淋叶。但是有兩種不同查詢方式阎曹,考慮如下代碼:
function(){
a = 5;
}
console.log(a) //5
a 是一個(gè)未聲明的變量,實(shí)際上會(huì)隱含地創(chuàng)建一個(gè)全局變量,再看另一個(gè):
let b = a煞檩; //TypeError: a is not defined
問題:同樣是查詢沒有聲明的變量处嫌,為什么結(jié)果不一樣?
答:對(duì)于變量斟湃,編譯器有兩種處理方式:LHS(left hand side)熏迹、RHS(right hand side)
LHS
賦值號(hào)左邊的查詢,實(shí)際上是找一個(gè)名叫 b 的容器凝赛,如果在當(dāng)前作用域沒有找到癣缅,
就向上一級(jí)作用域查詢,直到頂級(jí)作用域哄酝,如果頂級(jí)作用域還沒有友存,那就隱式創(chuàng)建一個(gè)并返回
RHS
賦值號(hào)右邊的查詢,“RHS”意味著“取得他/她的源(值)”陶衅,暗示著 RHS 的意思是“去取……的值”屡立。
考慮如下代碼:
function foo(a) {
console.log( a ); // 2
}
foo( 2 );
調(diào)用 foo(..)
的最后一行作為一個(gè)函數(shù)調(diào)用要求一個(gè)指向 foo
的 RHS 引用,意味著,“去查詢 foo
的值膨俐,并把它交給我”勇皇。另外,(..) 意味著 foo
的值應(yīng)當(dāng)被執(zhí)行焚刺,所以它最好實(shí)際上是一個(gè)函數(shù)敛摘!
這里有一個(gè)微妙但重要的賦值。你發(fā)現(xiàn)了嗎乳愉?
你可能錯(cuò)過了這個(gè)代碼段隱含的 a = 2
兄淫。它發(fā)生在當(dāng)值 2 作為參數(shù)值傳遞給 foo(..)
函數(shù)時(shí),值 2 被賦值 給了參數(shù) a
蔓姚。為了(隱含地)給參數(shù) a
賦值捕虽,進(jìn)行了一個(gè) LHS 查詢。
這里還有一個(gè) a
的值的 RHS 引用坡脐,它的結(jié)果值被傳入 console.log(..)泄私。console.log(..) 需要一個(gè)引用來執(zhí)行。它為 console 對(duì)象進(jìn)行一個(gè) RHS 查詢备闲,然后發(fā)生一個(gè)屬性解析來看它是否擁有一個(gè)稱為 log 的方法晌端。
小結(jié):
JavaScript 引擎 在執(zhí)行代碼之前首先會(huì)編譯它,因此恬砂,它將 var a = 2; 這樣的語句分割為兩個(gè)分離的步驟:
- 首先咧纠,var a 在當(dāng)前 作用域 中聲明。這是在最開始觉既,代碼執(zhí)行之前實(shí)施的。
- 稍后乳幸,a = 2 查找這個(gè)變量(LHS 引用)瞪讼,并且如果找到就向它賦值。
LHS 和 RHS 引用查詢都從當(dāng)前執(zhí)行中的 作用域 開始粹断,如果有需要(也就是符欠,它們?cè)谶@里沒能找到它們要找的東西),它們會(huì)在嵌套的 作用域 中一路向上瓶埋,一次一個(gè)作用域(層)地查找這個(gè)標(biāo)識(shí)符希柿,直到它們到達(dá)全局作用域(頂層)并停止,既可能找到也可能沒找到
詞法作用域
作用域的工作方式有兩種占統(tǒng)治地位的模型养筒。其中的第一種是最最常見曾撤,在絕大多數(shù)的編程語言中被使用的。它稱為 詞法作用域晕粪,我們將深入檢視它挤悉。另一種仍然被一些語言(比如 Bash 腳本,Perl 中的一些模式巫湘,等等)使用的模型装悲,稱為 動(dòng)態(tài)作用域昏鹃。
詞法作用域是 JavaScript 所采用的作用域模型。
看代碼:
function foo(a) {
var b = a * 2;
function bar(c) {
console.log( a, b, c );
}
bar(b * 3);
}
foo( 2 ); // 2 4 12
這里有三層作用域: 最外面一層诀诊、foo洞渤、bar
在上面的代碼段中,引擎 執(zhí)行語句 console.log(..) 并開始查找三個(gè)被引用的變量 a属瓣,b 和 c载迄。它首先從最內(nèi)部的作用域氣泡開始,也就是 bar(..) 函數(shù)的作用域奠涌。在這里它找不到 a宪巨,所以它向上走一層,到外面下一個(gè)最近的作用域氣泡溜畅,foo(..) 的作用域捏卓。它在這里找到了 a,于是它就使用這個(gè) a慈格。同樣的事情也發(fā)生在 b 身上怠晴。但是對(duì)于 c,它在 bar(..) 內(nèi)部就找到了
一旦找到第一個(gè)匹配浴捆,作用域查詢就停止了
注意:全局變量也自動(dòng)地是全局對(duì)象(在瀏覽器中是 window蒜田,等等)的屬性,所以不直接通過全局變量的詞法名稱选泻,而通過將它作為全局對(duì)象的一個(gè)屬性引用來間接地引用冲粤,是可能的
window.a
欺騙詞法作用域 eval()
詞法作用域是由函數(shù)被聲明的位置唯一定義的,而且這個(gè)位置完全是一個(gè)編寫時(shí)的決定页眯。
但是! 不完全是這樣梯捕!
JavaScript 中的 eval(..) 函數(shù)接收一個(gè)字符串作為參數(shù)值,并將這個(gè)字符串的內(nèi)容看作是好像它已經(jīng)被實(shí)際編寫在程序的那個(gè)位置上窝撵。
換句話說傀顾,你可以用編程的方式在你編寫好的代碼內(nèi)部生成代碼,而且你可以運(yùn)行這個(gè)生成的代碼碌奉,就好像它在編寫時(shí)就已經(jīng)在那里了一樣
function foo(str, a) {
eval( str ); // 作弊短曾!
console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1 3
默認(rèn)情況下,如果 eval(..) 執(zhí)行的代碼字符串包含一個(gè)或多個(gè)聲明(變量或函數(shù))的話赐劣,這個(gè)動(dòng)作就會(huì)修改這個(gè) eval(..) 所在的詞法作用域嫉拐。
-
當(dāng) eval(..) 被用于一個(gè)操作它自己的詞法作用域的 strict 模式程序時(shí),在 eval(..) 內(nèi)部做出的聲明不會(huì)實(shí)際上修改包圍它的作用域
function foo(str) { "use strict"; eval( str ); console.log( a ); // ReferenceError: a is not defined } foo( "var a = 2" );
JavaScript 引擎 在編譯階段期行許多性能優(yōu)化工作魁兼。其中的一些優(yōu)化原理都?xì)w結(jié)為實(shí)質(zhì)上在進(jìn)行詞法分析時(shí)可以靜態(tài)地分析代碼椭岩,并提前決定所有的變量和函數(shù)聲明都在什么位置,這樣在執(zhí)行期間就可以少花些力氣來解析標(biāo)識(shí)符。
但是判哥!使用eval()會(huì)讓編譯器假定自己知道的所有的標(biāo)識(shí)符的位置可能是無效的献雅,因?yàn)樗豢赡茉谠~法分析時(shí)就知道你將會(huì)向eval(..)傳遞什么樣的代碼來修改詞法作用域
盡可能避免使用 eval() ,為了性能塌计!
函數(shù)與塊兒作用域
隱藏于普通作用域
拿你所編寫的代碼的任意一部分挺身,在它周圍包裝一個(gè)函數(shù)聲明,這實(shí)質(zhì)上“隱藏”了這段代碼锌仅。
有多種原因驅(qū)使著這種基于作用域的隱藏章钾。它們主要是由一種稱為“最低權(quán)限原則”的軟件設(shè)計(jì)原則引起的note-leastprivilege
,有時(shí)也被稱為“最低授權(quán)”或“最少曝光”
將變量和函數(shù)“隱藏”在一個(gè)作用域內(nèi)部的另一個(gè)好處是热芹,避免兩個(gè)同名但用處不同的標(biāo)識(shí)符之間發(fā)生無意的沖突贱傀。沖突經(jīng)常導(dǎo)致值被意外地覆蓋。
函數(shù)作為作用域
我們可以拿來一段代碼并在它周圍包裝一個(gè)函數(shù)伊脓,而這實(shí)質(zhì)上對(duì)外部作用域“隱藏”了這個(gè)函數(shù)內(nèi)部作用域包含的任何變量或函數(shù)聲明
var a = 2;
function foo() { // <-- 插入這個(gè)
var a = 3;
console.log( a ); // 3
} // <-- 和這個(gè)
foo(); // <-- 還有這個(gè)
console.log( a ); // 2
雖然這種技術(shù)“可以工作”府寒,但它不一定非常理想。它引入了幾個(gè)問題报腔。首先是我們不得不聲明一個(gè)命名函數(shù) foo()株搔,這意味著這個(gè)標(biāo)識(shí)符名稱 foo 本身就“污染”了外圍作用域(在這個(gè)例子中是全局)。我們要不得不通過名稱(foo())明確地調(diào)用這個(gè)函數(shù)來使被包裝的代碼真正運(yùn)行纯蛾。
幸運(yùn)的是纤房,JavaScript 給這兩個(gè)問題提供了一個(gè)解決方法--IIFE
var a = 2;
(function foo(){ // <-- 插入這個(gè)
var a = 3;
console.log( a ); // 3
})(); // <-- 和這個(gè)
console.log( a ); // 2
當(dāng)然也可以這樣寫:
var a = 2;
(function IIFE( global ){
var a = 3;
console.log( a ); // 3
console.log( global.a ); // 2
})( window );
console.log( a ); // 2
塊兒作為作用域
我們經(jīng)常不經(jīng)意間聲明了全局變量:
for (var i=0; i<10; i++) {
console.log( i );
}
console.log(i) // 10
let
let 關(guān)鍵字將變量聲明附著在它所在的任何塊兒(通常是一個(gè) { .. })的作用域中。換句話說翻诉,let 為它的變量聲明隱含地劫持了任意塊兒的作用域炮姨。
var foo = true;
if (foo) {
{ // <-- 明確的塊兒
let bar = foo * 2;
bar = something( bar );
console.log( bar );
}
}
console.log( bar ); // ReferenceError
注意:var 會(huì)有聲明提前,然而碰煌,使用 let 做出的聲明將 不會(huì) 在它們所出現(xiàn)的整個(gè)塊兒的作用域中提升舒岸。如此,直到聲明語句為止拄查,聲明將不會(huì)“存在”于塊兒中
{
console.log( bar ); // ReferenceError!
let bar = 2;
}
關(guān)于 var 和 let
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000)
} // 隔一秒輸出吁津, 5 個(gè) 6
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000)
} // 隔一秒輸出 1堕扶,2,3梭依,4稍算,5
上面的代碼展示了 var 和 let 的不同。 let 聲明時(shí)保存了當(dāng)時(shí)的環(huán)境役拴,保留了每一個(gè)值糊探,下一次循環(huán)再在(另一個(gè)"塊")創(chuàng)建一個(gè)另外的變量
而 var 從頭到尾只有一個(gè)變量,并且輸出時(shí)值為 6