在上一篇文章中,小編介紹了詞法作用域,并在其中提到了兩個會出現(xiàn)“欺騙”詞法作用域的關(guān)鍵字——eval和with,今天小編就和大家一起揭開這兩個關(guān)鍵字的神秘面紗。在探索今天的內(nèi)容之前,先把上一篇文章的債還上。大家還可以關(guān)注我的微信公眾號,蝸牛全棧。
在上一篇文章中,我提到了【通過這種技術(shù)可以訪問那些被同名變量所遮蔽的全局變量,但非全局變量如果被遮蔽了,無論如何都無法被訪問到。】,下面的代碼沒有寫出來。也就是這樣:
var b = 3
function foo(){
var b = 4;
function bar(){
var b = 5;
console.log(b);
}
bar();
}
在這個函數(shù)中,可以通過調(diào)用函數(shù)foo,可以看到詞法作用域的遮蔽效應(yīng),在這個函數(shù)中,遮蔽了全局變量var b = 3,也遮蔽了在foo內(nèi)的var b=4;我們可以通過window.b來訪問到3,但是目前為止,我們還不能訪問到4
下面今天的干貨才正式開始:
如果詞法作用域完全由寫代碼期間函數(shù)所聲明的位置來定義,怎樣才能在運行時來“修改”(也可以說欺騙)詞法作用域呢?
JavaScript中有兩種機制來實現(xiàn)這個目的。社區(qū)普遍認(rèn)為在代碼中使用這兩種機制并不是什么好主意。但是關(guān)于他們的爭論通常會忽略掉最重要的點:欺騙詞法作用域會導(dǎo)致性能下降。
在詳細(xì)解釋性能問題之前,先來看看這兩種機制分別是什么原理。
一、eval
JavaScript中的eval函數(shù)可以接受一個字符串作為參數(shù),并將其中的內(nèi)容視為好像在書寫時就在于程序中的這個位置的代碼。換句話說,可以在你寫的代碼中用程序生成代碼并運行,就好像代碼是寫在那個位置的一樣?!酒鋵嵾@本身好像就影響了代碼的正常運行?!?/p>
根據(jù)這個原理來理解eval,它是如何通過代碼欺騙和假裝書寫時(也就是詞法期)代碼就在那,來實現(xiàn)修改詞法作用域環(huán)境的,這個原理就變得清晰易懂了。
在執(zhí)行eval之后的代碼時,引擎并不“知道”或者“在意”前面的代碼是以動態(tài)形式插入進(jìn)來【也就是通過eval函數(shù)內(nèi)部的代碼】,并對詞法作用域的環(huán)境進(jìn)行修改的。引擎只會如往常地進(jìn)行詞法作用域查找【就這么把引擎欺騙了】。
考慮以下代碼
function foo(a){
eval(str); // 欺騙【因為我們不知道str中會傳入什么,弄不好就是一個新的作用域】
console.log(a, b);
}
var b = 2;
foo(“var b=3”,1); // 1,3
eval調(diào)用中的“var b=3;”這段代碼會被當(dāng)作本來就在那里一樣來處理【這個時候,函數(shù)在引用的時候就變成了這樣】
function foo(a){
var b=3;
console.log(a, b);
}
var b = 2;
foo(1); // 1,3
由于那段代碼聲明了一個新的變量b,因此它對已經(jīng)存在的foo的詞法作用域進(jìn)行了修改。事實上,和前面提到的原理一樣,這段代碼實際上在foo內(nèi)部創(chuàng)建了一個變量b,并遮蔽了外部(全局)作用域中的同名變量。【是否還記得變量的遮蔽效應(yīng),不記得的話,可以翻看小編的上一篇文章】
當(dāng)console.log被執(zhí)行時,會在foo內(nèi)部同時找到a和b,但是永遠(yuǎn)也無法找到外部的b?!疽驗樵趀val內(nèi)部,建立了一個局部作用域,遮蔽了全局的b,如果要訪問全局的b,可以通過window.b訪問到】因此會輸出“1,3”,而不是正常情況下會輸出的“1,2”
【如果要是想強制的輸出“1,2”,我們可以頑皮的把代碼改成這樣】
function foo(a){
eval(str);
console.log(a, window.b); // 通過window,訪問的就是全局上的b變量的值
}
var b = 2;
foo(“var b=3”,1); // 1,2
在上面的例子中,為了展示的方便和簡潔,我們傳遞進(jìn)去的“代碼”字符串是固定不變的。而在實際情況中,可以非常容易的根據(jù)程序邏輯動態(tài)的將字符串拼接在一起之后再傳遞進(jìn)去。eval通常被用來執(zhí)行動態(tài)創(chuàng)建的代碼,因為像例子中這樣動態(tài)的執(zhí)行一段固定字符串所組成的代碼,并沒有比直接將代碼寫在那里更有好處。
在嚴(yán)格模式的程序中,eval在運行時有其自己的詞法作用域,意味著其中的聲明無法修改所在的作用域
fuction foo(str){
“use strict”;
eval(str);
console.log(a); // ReferenceError:a is not defined
}
foo(“var a = 2”);
JavaScript中還有其他一些功能效果和eval很相似。setTimeout和setInterval的第一個參數(shù)可以是字符串,字符串的內(nèi)容可以被解釋為一段動態(tài)生成的函數(shù)代碼。這些功能已經(jīng)過時并不被提倡。不要使用他們!

new Function函數(shù)的行為也很類似,最后一個參數(shù)可以接受代碼字符串,并將其轉(zhuǎn)化為動態(tài)生成的函數(shù)(前面的參數(shù)是這個新生成函數(shù)的形參)。這種構(gòu)建函數(shù)的語法比eval略微安全一些,但是也要盡量避免使用
在程序中動態(tài)生成代碼的使用場景非常罕見,因為它所帶來的好處無法抵消性能上的損失。
二、withJavaScript中另一個難以掌握(并且現(xiàn)在也不推薦使用)的用來欺騙詞法作用域的功能是with關(guān)鍵字??梢杂泻芏喾椒▉斫忉寃ith

在這里我選擇從這個角度解釋它:它如何同被它所影響的詞法作用域進(jìn)行交互。with通常被當(dāng)作重復(fù)引用同一個對象中的多個屬性的快捷方式,可以不需要重復(fù)引用對象本身。
比如:
var obj = {
a: 1,
b: 2,
c: 3
};
// 單調(diào)乏味的重復(fù)“obj”
obj.a = 2;
obj.b = 3;
obj.c = 4;
// 簡單的快捷方式
with(obj){
a=3;
b=4;
c=5;
}
但實際上這不僅僅是為了方便地訪問對象屬性??紤]如下代碼:
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被泄漏到全局作用域
這個例子中創(chuàng)建了o1和o2兩個對象。其中一個具有a屬性,另外一個沒有。foo函數(shù)接受一個obj參數(shù),該參數(shù)是一個對象引用,并對這個對象引用執(zhí)行了with(obj){}。在with塊內(nèi)部,我們寫的代碼看起來只是對變量a進(jìn)行簡單的詞法引用,實際上就是一個LHS引用,并將2賦值給它。
當(dāng)我們將o1傳遞進(jìn)去,a=2賦值操作找到了o1.a并將2賦值給它,這在后面的console.log(o1.a)中可以體現(xiàn)。而當(dāng)o2傳遞進(jìn)去,o2并沒有a屬性,因此不會創(chuàng)建這個屬性,o2.a保持undefined。
但是可以注意到一個奇怪的副作用,實際上a=2賦值操作創(chuàng)建了一個全局的變量a。這是怎么回事呢?
with可以將一個沒有或有多個屬性的對象處理為一個完全隔離的詞法作用域,因此這個對象的屬性也會被處理為定義在這個作用域中的詞法標(biāo)識符。
盡管with塊可以將一個對象處理為詞法作用域,但是這個塊內(nèi)部正常的var聲明并不會被限制在這個塊的作用域中,而是被添加到with所處的函數(shù)作用域中。
eval函數(shù)如果接受了含有一個或多個聲明的代碼,就會修改其所處的詞法作用域,而with聲明實際上是根據(jù)你傳遞給它的對象憑空創(chuàng)建了一個全新的詞法作用域。
可以這樣理解,當(dāng)我們傳遞o1給with時,with所聲明的作用域是o1,而這個作用域中含有一個同o1.a屬性相符的標(biāo)識符。但當(dāng)我們將o2作為作用域時,其中并沒有a標(biāo)識符,因此進(jìn)行了正常了LHS標(biāo)識查找。
o2的作用域、foo的作用域和全局作用域中都沒有找到標(biāo)識符a,因此當(dāng)a=2執(zhí)行時,自動創(chuàng)建了一個全局變量(因為是非嚴(yán)格模式)【在嚴(yán)格模式下,會報出ReferenceError】
with這種講對象及其屬性放進(jìn)一個作用域并同時分配標(biāo)識符的行為很讓人費解。但為了說明我們所看到的現(xiàn)象,這是我能給出的最直白的解釋了。另外一個不推薦使用eval和with的原因是會被嚴(yán)格模式所影響(限制)。
with被完全禁止,而在保留核心功能的前提下,簡潔或非安全的使用eval也被禁止了。
三、性能
eval和with會在運行時修改或創(chuàng)建新的作用域,以此來欺騙其他在書寫時定義的詞法作用域。
你可能會問,那又怎樣呢?如果他們能實現(xiàn)更復(fù)雜的功能,并且代碼更具有擴(kuò)展性,難道不是非常好的功能嗎?答案是否定的。
JavaScript引擎會在編譯階段進(jìn)行數(shù)項的性能優(yōu)化。其中有些優(yōu)化依賴于能夠根據(jù)代碼的詞法進(jìn)行靜態(tài)分析,并預(yù)先確定所有變量和函數(shù)的定義位置,才能在執(zhí)行過程中快速找到標(biāo)識符。
但如果引擎在代碼中發(fā)現(xiàn)了eval或with,他只能簡單地假設(shè)關(guān)于標(biāo)識符位置的判斷都是無效的,因為無法在詞法分析階段明確知道eval會接收到什么代碼,這些代碼會如何對作用域進(jìn)行修改,也無法知道傳遞給with用來創(chuàng)建新詞法作用域的對象的內(nèi)容到底是什么。
最悲觀的情況是如果出現(xiàn)了eval或with,所有的優(yōu)化可能都是無意義的,因此最簡單的做法就是完全不做任何優(yōu)化。【因為中間有太多的不確定性,那最好的辦法就是不動,保持原狀】
如果代碼中大量使用eval或with,那么運行起來一定會變得非常慢。無論引擎多聰明,試圖將這些悲觀情況的副作用限制在最小范圍內(nèi),也無法避免如果這些優(yōu)化,代碼會運行的更慢的這個事實。