昨天圣誕節(jié),沒(méi)有寫,今天寫兩篇。奧利給!---現(xiàn)在是2021-1-4 還好不算晚,才隔了幾天。
詞法作用域
在第一章中,我們將“作用域”定義為一套規(guī)則,這套規(guī)則用來(lái)管理引擎如何在當(dāng)前作用 域以及嵌套的子作用域中根據(jù)標(biāo)識(shí)符名稱進(jìn)行變量查找。
詞法階段
大部分標(biāo)準(zhǔn)語(yǔ)言編譯器的第一個(gè)工作階段叫作詞法化(也叫單詞化)?;?憶一下,詞法化的過(guò)程會(huì)對(duì)源代碼中的字符進(jìn)行檢查,如果是有狀態(tài)的解析過(guò)程,還會(huì)賦 予單詞語(yǔ)義。
這個(gè)概念是理解詞法作用域及其名稱來(lái)歷的基礎(chǔ)。
簡(jiǎn)單的說(shuō),詞法作用域就是定義在詞法階段的作用域。
換句話說(shuō),詞法作用域是由你在寫 代碼時(shí)將變量和塊作用域?qū)懺谀睦飦?lái)決定的,因此當(dāng)詞法分析器處理代碼時(shí)會(huì)保持作用域 不變(大部分情況下是這樣的)。
看下面的例子:
function foo(a){//-----------1
var b = a * 2;//---------2
function bar(c){//-------3
console.log(a,b,c);
}
bar(b * 3);
}
foo(2);//2,4,12
考慮上面的代碼,有三個(gè)逐級(jí)嵌套的作用域。讓我們來(lái)一一說(shuō)明一下。
- 包含著整個(gè)全局作用域,其中一個(gè)標(biāo)識(shí)符:
foo。 - 包含著
foo所創(chuàng)建的作用域,其中有三個(gè)標(biāo)識(shí)符:a,bar,b. - 包含著
bar所創(chuàng)建的作用域,其中只有一個(gè)標(biāo)識(shí)符c.
可以看到bar的作用域完全包含在foo所創(chuàng)建的氣泡中。
查找
作用域氣泡的結(jié)構(gòu)和互相之間的位置關(guān)系給引擎提供了足夠的位置信息,引擎用這些信息來(lái)查找標(biāo)識(shí)符的位置。
作用域查找始終會(huì)從運(yùn)行時(shí)所處的最內(nèi)部作用域開始,逐級(jí)向外部或者向上進(jìn)行,知道遇見第一個(gè)匹配的標(biāo)識(shí)符為止。
作用域查找會(huì)在找到的第一個(gè)匹配的標(biāo)識(shí)符時(shí)停止。在多層的嵌套作用域中可以定義同名的 標(biāo)識(shí)符,這叫作“遮蔽效應(yīng)”(內(nèi)部的標(biāo)識(shí)符“遮蔽”了外部的標(biāo)識(shí)符)。
全局變量會(huì)自動(dòng)成為全局對(duì)象(比如瀏覽器中的window對(duì)象)的屬性。因此可以不直接通過(guò)全局對(duì)象的詞法名稱,而是間接地通過(guò)全局對(duì)象的引用來(lái)對(duì)其進(jìn)行訪問(wèn)。
window.a
通過(guò)這種技術(shù)可以訪問(wèn)那些被同名變量所遮蔽的全局變量。但非全局的變量如果被遮蔽了,無(wú)論如何都無(wú)法被訪問(wèn)到。
請(qǐng)記住這一點(diǎn):
無(wú)論函數(shù)在哪里被調(diào)用,也無(wú)論如何被調(diào)用。它的詞法作用域都只由函數(shù)所聲明時(shí)所處的位置決定。
欺騙詞法作用域
如果詞法作用域完全由寫代碼期間函數(shù)所聲明的位置來(lái)定義,怎樣才能在運(yùn)行時(shí)來(lái)修改(欺騙)詞法作用域呢?
有兩種機(jī)制可以做到,但是都強(qiáng)烈不推薦使用。
欺騙詞法作用域會(huì)導(dǎo)致性能下降。
eval 函數(shù)
eval函數(shù)可以接受一個(gè)字符串為參數(shù),并將其中的內(nèi)容視為好像在書寫時(shí)就存在于程序中的這個(gè)位置的代碼。
也就是說(shuō),你可以在你寫的代碼中用程序生成代碼并運(yùn)行,就好像代碼是寫在那個(gè)位置的一樣。
根據(jù)這個(gè)原理來(lái)理解eval(...),它是如何通過(guò)代碼欺騙和假裝書寫時(shí)也就是詞法期代碼就在,來(lái)實(shí)現(xiàn)修改詞法作用環(huán)境的。這個(gè)原理可謂簡(jiǎn)單易懂。
在執(zhí)行 eval(..) 之后的代碼時(shí),引擎并不“知道”或“在意”前面的代碼是以動(dòng)態(tài)形式插 入進(jìn)來(lái),并對(duì)詞法作用域的環(huán)境進(jìn)行修改的。引擎只會(huì)如往常地進(jìn)行詞法作用域查找。
function foo(str,a){
eval(str);
console.log(a,b);
}
var b = 2;
foo("var b = 3",1);
//VM358:2 Uncaught EvalError: Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive:
我們?cè)?code>foo函數(shù)中使用eval函數(shù),把str變量包裹,做了處理。所以在調(diào)用foo函數(shù)的時(shí)候,我們第一個(gè)參數(shù)傳入的是"var b = 3",經(jīng)過(guò)eval的處理以后,我們?cè)?code>eval函數(shù)的位置真的聲明一個(gè)b的變量。并且遮蔽了外部(全局)作用域中同名變量。
在JavaScript中還有一些其他的功能效果和eval很相似
-
setTimeout(...)和setInterval(...)的第一個(gè)參數(shù)可以字符串。字符串的內(nèi)容可以被解釋為一段動(dòng)態(tài)生成的 函數(shù)代碼。這 -
new Function(...)最后一個(gè)參數(shù)可以接受代碼字符串,并將其轉(zhuǎn) 化為動(dòng)態(tài)生成的函數(shù)(前面的參數(shù)是這個(gè)新生成的函數(shù)的形參)。
不要這樣玩。
with 函數(shù)
with 通常被當(dāng)作重復(fù)引用同一個(gè)對(duì)象中的多個(gè)屬性的快捷方式,可以不需要重復(fù)引用對(duì)象 本身。
var obj = { a: 1, b: 2, c: 3 };
// 單調(diào)乏味的重復(fù)"obj"
obj.a = 2;
obj.b = 3;
obj.c = 4;
// 簡(jiǎn)單的快捷方式
with (obj) {
a = 3;
b = 4;
c = 5
}
但實(shí)際上這不僅僅是為了方便地訪問(wèn)對(duì)象屬性??紤]如下代碼:
function foo(obj) {
with (obj) {
a = 2;
}
}
var o1 = { a: 3 };
var o2 = { b: 3 };
foo( o1 );
console.log( o1.a ); // 2 foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2——不好,a 被泄漏到全局作用域上了!
這個(gè)例子中創(chuàng)建了 o1 和 o2 兩個(gè)對(duì)象。其中一個(gè)具有 a 屬性,另外一個(gè)沒(méi)有。foo(..) 函 數(shù)接受一個(gè) obj 參數(shù),該參數(shù)是一個(gè)對(duì)象引用,并對(duì)這個(gè)對(duì)象引用執(zhí)行了 with(obj) {..}。 在 with 塊內(nèi)部,我們寫的代碼看起來(lái)只是對(duì)變量 a 進(jìn)行簡(jiǎn)單的詞法引用,實(shí)際上就是一個(gè) LHS 引用(查看第 1 章),并將 2 賦值給它。
當(dāng)我們將 o1 傳遞進(jìn)去,a=2 賦值操作找到了 o1.a 并將 2 賦值給它,這在后面的 console. log(o1.a) 中可以體現(xiàn)。而當(dāng) o2 傳遞進(jìn)去,o2 并沒(méi)有 a 屬性,因此不會(huì)創(chuàng)建這個(gè)屬性, o2.a 保持 undefined。
但是可以注意到一個(gè)奇怪的副作用,實(shí)際上 a = 2 賦值操作創(chuàng)建了一個(gè)全局的變量 a。這 是怎么回事?
with 可以將一個(gè)沒(méi)有或有多個(gè)屬性的對(duì)象處理為一個(gè)完全隔離的詞法作用域,因此這個(gè)對(duì) 象的屬性也會(huì)被處理為定義在這個(gè)作用域中的詞法標(biāo)識(shí)符。
盡管 with 塊可以將一個(gè)對(duì)象處理為詞法作用域,但是這個(gè)塊內(nèi)部正常的 var 聲明并不會(huì)被限制在這個(gè)塊的作用域中,而是被添加到 with 所處的函數(shù)作 用域中。
eval(..) 函數(shù)如果接受了含有一個(gè)或多個(gè)聲明的代碼,就會(huì)修改其所處的詞法作用域,而 with 聲明實(shí)際上是根據(jù)你傳遞給它的對(duì)象憑空創(chuàng)建了一個(gè)全新的詞法作用域.
可以這樣理解,當(dāng)我們傳遞 o1 給 with 時(shí),with 所聲明的作用域是 o1,而這個(gè)作用域中含 有一個(gè)同 o1.a 屬性相符的標(biāo)識(shí)符。但當(dāng)我們將 o2 作為作用域時(shí),其中并沒(méi)有 a 標(biāo)識(shí)符, 因此進(jìn)行了正常的 LHS 標(biāo)識(shí)符查找(查看第 1 章)。
o2 的作用域、foo(..) 的作用域和全局作用域中都沒(méi)有找到標(biāo)識(shí)符 a,因此當(dāng) a=2 執(zhí)行 時(shí),自動(dòng)創(chuàng)建了一個(gè)全局變量(因?yàn)槭欠菄?yán)格模式)
with 這種將對(duì)象及其屬性放進(jìn)一個(gè)作用域并同時(shí)分配標(biāo)識(shí)符的行為很讓人費(fèi)解。但為了說(shuō) 明我們所看到的現(xiàn)象,這是我能給出的最直白的解釋了。
另外一個(gè)不推薦使用 eval(..) 和 with 的原因是會(huì)被嚴(yán)格模式所影響(限 制)。with 被完全禁止,而在保留核心功能的前提下,間接或非安全地使用 eval(..) 也被禁止了。
小結(jié):
詞法作用域意味著作用域是由書寫代碼時(shí)函數(shù)聲明的位置來(lái)決定的。編譯的詞法分析階段 基本能夠知道全部標(biāo)識(shí)符在哪里以及是如何聲明的,從而能夠預(yù)測(cè)在執(zhí)行過(guò)程中如何對(duì)它 們進(jìn)行查找。
JavaScript 中有兩個(gè)機(jī)制可以“欺騙”詞法作用域:eval(..) 和 with。前者可以對(duì)一段包 含一個(gè)或多個(gè)聲明的“代碼”字符串進(jìn)行演算,并借此來(lái)修改已經(jīng)存在的詞法作用域(在 運(yùn)行時(shí))。后者本質(zhì)上是通過(guò)將一個(gè)對(duì)象的引用當(dāng)作作用域來(lái)處理,將對(duì)象的屬性當(dāng)作作 用域中的標(biāo)識(shí)符來(lái)處理,從而創(chuàng)建了一個(gè)新的詞法作用域(同樣是在運(yùn)行時(shí))。
這兩個(gè)機(jī)制的副作用是引擎無(wú)法在編譯時(shí)對(duì)作用域查找進(jìn)行優(yōu)化,因?yàn)橐嬷荒苤?jǐn)慎地認(rèn) 為這樣的優(yōu)化是無(wú)效的。使用這其中任何一個(gè)機(jī)制都將導(dǎo)致代碼運(yùn)行變慢。不要使用它們。