1. 程式人生 > >你不知道的JavaScript--作用域(二)

你不知道的JavaScript--作用域(二)

第二部分:詞法作用域

  • 詞法階段
  • 欺騙詞法(兩個機制:eval(…)和with)
  • 效能
  • 小結

詞法階段

詞法作用域是定義在詞法階段的作用域。

  • 作用域查詢在找到第一個匹配的識別符號時停止。—》遮蔽效應
  • 全域性變數會自動成為全域性物件(eg:瀏覽器中的window物件)的屬性。因此,可以不直接通過全域性物件的詞法名稱,而是間接地通過對全域性物件屬性的引用來對其訪問。//window.a
  • 通過這種技術可以訪問那些被同名變數遮蔽的全域性變數。但非全域性變數如果被遮蔽了,無論如何都無法被訪問到。
  • 詞法作用域只會找一級識別符號。

欺騙詞法:

欺騙詞法的兩個機制:

  • eval(…)
  • with

1) eval(…)

  • 該函式可以接受一個字串為引數,並將其中的內容視為好像在書寫時就存在於程式中這個位置的程式碼(可以在你寫的程式碼中用程式生成程式碼並執行,就好像是寫在那個位置一樣)
  • 如果eval()中所執行的程式碼包含一個或者多個宣告(無論是變數還是函式),就會對eval()所處的詞法作用域進行修改。
function foo(str,a){
    eval(str);//欺騙
    console.log(a,b);
}
var b = 2;
foo("var b = 3;",1)

//結果是1,3
  • 在嚴格模式的程式中,eval(…)在執行時有其自己的詞法作用域,意味著其中的宣告無法修改所在的作用域。
function foo(str){
    "use strict";
    eval(str);
    console.log(a); // ReferenceError : a is not defined
}
foo("var a = 2;");
  • js中還有其他一些功能效果和eval相似,setTimeout(…)和setInterval(…)的第一個引數可以是字串,字串的內容可以被解釋為一段動態生成的函式程式碼。//這些功能已經過時並且不被提倡,不要使用它們!
  • new Function(…)函式的行為也很類似,最後一個引數可以接受程式碼字串,並將其轉換為動態生成的函式(前面的引數是這個新生成的函式的形參)。這狗構建函式的語法比eval略微安全一些,但也要儘量避免使用。

2)with

  • 通常被當作重複引用同一個物件中的多個屬性的 快捷方式,可以不用重複引用物件本身。
  • 通過講一個物件的引用當作當前作用域來處理,從而建立一個新的詞法作用域
var obj = {
    a:1,
    b:2,
    c:3
};
//單調乏味的重複"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被洩露到全域性作用域上了!
  • 在with塊內部,看起來只是對a進行簡單的詞法引用,實際上就是一個LHS引用。
  • ①程式碼塊很容易理解:將o1傳遞進去,a=2賦值操作找到了o1.a,並將2賦值給它。
  • ②程式碼塊 第一個輸出語句:當o2傳遞進去,o2並沒有a屬性,因此不會建立這個屬性,o2.a保持undefined。
  • ②程式碼塊 第二個輸出語句:將o2作為作用域時,其中並沒有a識別符號,因此進行了正常的LHS識別符號查詢。foo和全域性作用域都沒有找到識別符號a,當a=2執行時,自動建立了一個全域性變數(因為是費嚴格模式,嚴格模式下會報錯。)
  • with這種將物件及其屬性放進一個作用域並同時分配識別符號的行為很讓人費解……但這就是我們所看到的現象。

效能

  • eval(……)和with會在執行時修改或建立新的作用域,以此來欺騙其他在書寫時定義的詞法作用域。
  • js引擎會在編譯階段進行數項的效能優化。
  • 其中有些優化依賴於能夠根據程式碼的詞法進行靜態分析,並預先確定所有變數和函式的定義位置,才能在執行過程中快速找到識別符號。
  • 但如果引擎在程式碼中發現了eval(……)和with,它只能簡單地假設關於識別符號位置的判斷都是無效的。因為無法在詞法分析階段明確知道eval(……)會接收什麼diamante,這些程式碼會如何對作用域進行修改,也無法知道傳遞給with用來建立新的詞法作用域的物件的內容到底是什麼。出現了eval(……)和with,所有的優化都是無意義的,因此,最簡單的做法是完全不做任何優化。大量的使用eval(……)和with,執行起來一定會變得非常慢。

小結

  1. 詞法作用域意味著作用域是由書寫程式碼時函式宣告的位置來決定的。
  2. 編譯的詞法分析階段基本能夠知道全部識別符號住哪裡,如何宣告。
  3. 欺騙詞法作用域會導致新能下降,js引擎在編譯時對作用域進行優化,因為引擎只能謹慎地認為這樣的優化是無效的。任何一個欺騙詞法機制都將導致程式碼執行變慢。不要使用他們。
  4. 另一個不推薦使用eval(…)和with的原因是會被嚴格模式所影響(限制)。with被完全禁止,在保留核心功能的前提下,間接或非安全地使用eval(…)也被禁止了。
  • 關於欺騙此法對效能的影響,如今的瀏覽器大都做了優化:

在舊的瀏覽器中如果你使用了eval,效能會下降10倍。
在現代瀏覽器中有兩種編譯模式:fast path和slow path。fast path是編譯那些穩定和可預測(stable and predictable)的程式碼。
它由2個能夠將原始碼直接轉換成計算機程式碼的編譯器組成。

Full-codegen:輸出非優化程式碼的快速編譯器。
Crankshaft:輸出快速,優化程式碼的慢速編譯器。

如果Crankshaft認為由Full-codegen產生的不夠優化的程式碼需要優化,它將會取而代之,這就是“crankshafting”流程。