1. 程式人生 > >二、JS詞法作用域--查詢--欺騙詞法

二、JS詞法作用域--查詢--欺騙詞法

作用域共有兩種主要的工作模型。第一種是最為普遍的,被大多數程式語言所採用的詞法作用域。
另一種叫作動態作用域,仍有一些程式語言在使用(比如Bash指令碼、Perl中的一些模式等)。

詞法作用域意味著作用域是由書寫程式碼是函式宣告的位置來決定的。編譯的詞法分析階段基本能夠直到全部識別符號在哪裡以及是如何宣告的,從而能夠預測在執行工程中如何對它們進行查詢。

JavaScript中有兩個機制可以“欺騙”詞法作用域:eval(…)和with。前者可以對一段包含一個或多個宣告的”程式碼“字串進行演算,並藉此來修改已經存在的詞法作用域(在執行時)。後者本質上是通過將一個物件的引用當作作用域來處理,將物件的屬性當作作用域中的識別符號來處理,從而建立了一個新的詞法作用域(同樣是在執行時)。

這兩個機制的副作用時引擎無法在編譯時對作用域查詢進行優化,因此引擎只能謹慎地認為這樣的優化是無效的的。使用這其中任何一個機制都將導致程式碼執行變慢。

詞法作用域

大部分標準語言編譯器的第一個工作階段叫作詞法化(也叫單詞化)。
詞法作用域就是定義在詞法階段的作用域。換句話說,詞法作用域是由你在寫程式碼時將變數和塊作用域寫在哪裡來決定的,因此當詞法分析器處理程式碼時會保持作用域不變(大部分情況下是這樣的)。

查詢

作用域氣泡的結構和互相之間的位置關係給引擎提供了足夠的位置資訊,引擎利用這些資訊來查詢識別符號的位置。

作用域查詢會在找到第一個匹配的識別符號時停止。在多層的巢狀作用中可以定義同名的識別符號,這叫做”遮蔽效應“

(內部的識別符號”遮蔽“了外部的識別符號)。
拋開遮蔽效應,作用域查詢試重會執行時說出的最內部作用域開始,逐級向外或者向上進行,直到遇見第一個匹配的識別符號為止。

全域性變數會自動成為全域性物件(比如瀏覽器中的window物件)的屬性,因此可以不直接通過全域性物件的詞法名稱,而是間接的通過對全域性物件屬性的引用來對其進行訪問。

全域性變數會自動成為全域性物件(比如瀏覽器中的window物件)的屬性,因此可以不直接通過全域性物件的詞法名稱,而是間接的通過對全域性物件屬性的引用來對其進行訪問。
通過這種技術可以訪問那些被同名變數所遮蔽的全域性變數。但非全域性的變數如果被遮蔽了,無論如何都無法被訪問到。
無論函式在哪裡被呼叫,也無論它如何被呼叫,它的詞法作用域都只由函式被宣告時所處的位置決定。
詞法作用域只會查詢一級標題,比如a、b、c。如果程式碼中引用了foo.bar.baz,詞法作用域查詢只會試圖查詢foo識別符號,找到這個變數後,物件屬性訪問規則會分別接管對bar和baz屬性的訪問。

欺騙詞法

如果敵法作用域完全由寫程式碼期間函式所宣告的位置來定義,怎樣才能在執行時來”修改“(也可以說欺騙)詞法作用域。
JS中有兩種機制來實現這個目的。欺騙詞法作用域會導致效能下降。

  1. eval
    JS中的eval(..)函式可以接受一個字串為引數,並將其中的內容是為好像在書寫時就存在於程式這個位置的程式碼。換句話說,可以在寫的程式碼總用程式生成程式碼並執行,就好像程式碼是寫在那個位置的一樣。
    eval(..)通常被用來執行動態建立的程式碼。預設情況下,如果eval(..)中所執行的程式碼包含一個或多個宣告(無論是變數還是函式),就會對eval(..)所處的詞法作用域進行修改。
    無論在何種情況,eval(..)都可以在執行期修改書寫期的詞法作用域。

    嚴格模式的程式中,eval(..)在執行時有其自己的詞法作用域,意味著其中的宣告無法修改所在的作用域。

function foo(){
    "use strict";
    eval(str);
    console.log(a);//ReferenceError : a is not defind
}
foo("var a=2");

JS中還有其他的一些功能效果和eval(..)很相似。setTimeout(..)和setInterval(..)的第一個引數可以是字串,字串的內容可以被解釋為一段動態生成的函式程式碼。這些功能不被提倡(過時)。
new Function(..)函式的行為也很類似,最後一個引數可以接受程式碼字串,並將其轉化為動態生成的函式(前面的引數是這個新生產的函式的形參)。這種構建函式的語法比eval(..)略微安全一些,但也要儘量避免。
在程式中動態生成程式碼的使用場景非常罕見,因此它所帶來的好處無法抵消效能上的損失。

  1. with
    JS中另一個用來欺騙詞法作用域的功能是with關鍵字。可以有很多方法來解釋with,在這裡從這個角度進行解釋:它如何同被它所影響的詞法作用域進行互動。

with通常被當作重複引用同一個物件中的多個屬性的快捷方式,可以不需要重複引用物件本身。

function foo(obj){
    with (obj){
        a=2;
    }
}
var 01={
    a:3
};
var 02={
    b:3
};
foo(01);
console.log(01.a);//2

foo(02);
console.log(02.a);//undefined
console.log(a);//2--a被洩露到全域性作用域上了

這個例子中拆個年間了01和02兩個物件。其中一個具有a屬性,另外一個沒有。foo(..)函式接受一個obj引數,該引數是一個物件的引用,並對這個物件引用執行了with(obj){..}。在with塊內部,程式碼看起來只是對變數a進行了簡單的詞法引用,實際上就是一個LHS引用,並將2賦值給它。
當將01傳遞進去,a=2賦值操作找到了01.a,並將2賦值給它,這在後面console.log(01.a)中可以體現。而當02傳遞進去,02並沒有a屬性,因此不會建立這個屬性,02.a保持undefined。
with可以將一個沒有或有多個屬性的物件處理為一個完全隔離的詞法作用域,因此這個物件屬性也會被處理為定義在這個作用域中的詞法識別符號。

02的作用域、foo(..)的作用域和全域性作用域總都沒有找到識別符號a,因此當a=2執行時,自動建立了一個全域性變數(因為是非嚴格模式)。

儘管with塊可以將一個物件處理為詞法作用域,但是這個塊內部正常的var宣告並不會被限制在這個塊的作用域中,而是被新增到with所處的函式作用域中。

eval(..)函式如果接受了一個或多個宣告的程式碼,就會修改其所處的詞法作用域,而with宣告實際上是根據你傳遞給它的物件平憑空建立了一個全新的詞法作用域。
另外一個不推薦使用eval(..)和with原因是會被嚴格模式所影響(限制)。with被完全禁止,而在保留核心功能的前提下,間接或非安全的使用eval(..)也被禁止了。