你不懂js系列學習筆記-做用域和閉包- 02

第2章:詞法做用域

原文:You-Dont-Know-JSjavascript

做用域共有兩種主要的工做模型:java

  1. 第一種是最爲廣泛的,被大多數編程語言所採用的詞法做用域
  2. 另一種叫做動態做用域,仍有一些編程語言在使用(好比 Bash 腳本、Perl 中的一些模式等)。

詞法做用域 和 動態做用域的區別:git

function foo() {
    console.log( a ); // 2
  }
  function bar() {
    var a = 3;
	foo();
  }
  var a = 2;
  bar();
複製代碼

以上代碼:詞法做用域讓 foo() 中的 a 經過 RHS 引用到了全局做用域中的 a,所以會輸出 2。 JavaScript只有詞法做用域github

若是是動態做用域 :由於當 foo() 沒法找到 a 的變量引用時,會順着調用棧在調用 foo() 的地方查找 a,而不是在嵌套的詞法做用域鏈中向上查找。因爲 foo() 是在 bar() 中調用的,引擎會檢查 bar() 的做用域,並在其中找到值爲 3 的變量 a。chrome

須要明確的是,事實上 JavaScript 並不具備動態做用域。它只有詞法做用域,簡單明瞭。可是 this 機制某種程度上很像動態做用域。編程

主要區別:詞法做用域是在寫代碼或者說定義時肯定的,而動態做用域是在運行時肯定的。(this 也是!)詞法做用域關注函數在何處聲明,而動態做用域關注函數從何處調用。瀏覽器

1 詞法階段

  1. 包含着整個全局做用域,其中只有一個標識符:foo。
  2. 包含着 foo 所建立的做用域,其中有三個標識符:a、bar 和 b。 . 包含着 bar 所建立的做用域,其中只有一個標識符:c。

1.1 查找

以上代碼的查找過程:安全

在上一個代碼片斷中,引擎執行 console.log(..) 聲明,並查找 a、b 和 c 三個變量的引用。它首先從最內部的做用域,也就是 bar(..) 函數的做用域氣泡開始查找。引擎沒法在這裏找到 a,所以會去上一級到所嵌套的 foo(..) 的做用域中繼續查找。在這裏找到了 a,所以引擎使用了這個引用。對 b 來說也是同樣的。而對 c 來講,引擎在 bar(..) 中就找到了它。性能優化

若是 a、c 都存在於 bar(..) 和 foo(..) 的內部,console.log(..) 就能夠直接使用 bar(..)中的變量,而無需到外面的 foo(..) 中查找。編程語言

  1. 做用域查找會在找到第一個匹配的標識符時中止。(外部被遮蔽)
  2. 不管函數在哪裏被調用,也不管它如何被調用,它的詞法做用域都只由函數被聲明時所處 的位置決定。
  3. 詞法做用域查找只會查找一級標識符

2 欺騙詞法做用域

在運行時「修改」詞法做用域 JavaScript 有兩種這樣的機制:eval 和 with。(正常應用場景不多,並且會影響性能在代碼中應當被避免。)

2.1 eval

JavaScript 中的 eval(..) 函數接收一個字符串做爲參數值,並將這個字符串的內容看做是好像它已經被實際編寫在程序的那個位置上。換句話說,你能夠用編程的方式在你編寫好的代碼內部生成代碼,並且你能夠運行這個生成的代碼,就好像它在編寫時就已經在那裏了同樣。

eval(..) 被執行的後續代碼行中,引擎 將不會「知道」或「關心」前面的代碼是被動態翻譯的,並且所以修改了詞法做用域環境。引擎 將會像它一直作的那樣,簡單地進行詞法做用域查詢。

考慮以下代碼:(非 strict 模式)

function foo(str, a) {
	eval( str ); // 做弊!
	console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1 3

// 1,執行 foo();
// 2,執行 eval(str);
// 3,在 eval(..) 調用的位置上 生成var b = 3,修改了現存的 foo(..) 的詞法做用域
// 4,執行console.log(..) 在foo(..) 的做用域中找到 a 和 b (並不會在全局做用域中查找)
複製代碼

strict 模式下會報錯:

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

2.2 with

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 -- 哦,全局做用域被泄漏了!

// 1,執行foo(o1) 賦值 a = 2 , 找到屬性 o1.a, o1.a = 2
// 2,執行foo(o2) 賦值 a = 2 , o2 沒有 a 屬性 o2.a = undefined, 
//(這裏賦值 a = 2 建立了一個全局變量 a,若是 a = 2 加 var 則a屬於foo函數的做用域)
複製代碼

「做用域」 o2 中沒有,foo(..) 的做用域中也沒有,甚至連全局做用域中都沒有找到標識符 a,因此當 a = 2 被執行時,其結果就是自動全局被建立(由於咱們沒有在 strict 模式下)。

3 性能

JavaScript引擎在編譯階段期行許多性能優化工做。其中的一些優化原理都歸結爲實質上在進行詞法分析時能夠靜態地分析代碼,並提早決定全部的變量和函數聲明都在什麼位置,這樣在執行期間就能夠少花些力氣來解析標識符。

但若是引擎在代碼中發現一個 eval(..)with,它實質上就不得不假定本身知道的全部的標識符的位置多是無效的,由於它不可能在詞法分析時就知道你將會向eval(..)傳遞什麼樣的代碼來修改詞法做用域,或者你可能會向with傳遞的對象有什麼樣的內容來建立一個新的將被查詢的詞法做用域。

換句話說,悲觀地看,若是 eval(..)with 出現,那麼它將作的幾乎全部的優化都會變得沒有意義,因此它就會簡單地根本不作任何優化。

在舊的瀏覽器中若是你使用了eval,性能會降低10倍。在現代瀏覽器中有兩種編譯模式:fast pathslow pathfast path 是編譯那些穩定和可預測(stable and predictable)的代碼。而明顯的,eval不可預測,因此將會使用slow path ,因此會慢。

使用with關鍵字或者eval(..)對性能的影響還有一點就是js壓縮工具,它沒法對代碼進行壓縮,這也是影響性能的一個因素。

複習

詞法做用域意味着做用域是由編寫時函數被聲明的位置的決策定義的。編譯器的詞法分析階段實質上能夠知道全部的標識符是在哪裏和如何聲明的,並如此在執行期間預測它們將如何被查詢。

在 JavaScript 中有兩種機制能夠「欺騙」詞法做用域:eval(..)with。前者能夠經過對一個擁有一個或多個聲明的「代碼」字符串進行求值,來(在運行時)修改現存的詞法做用域。後者實質上是經過將一個對象引用看做一個「做用域」,並將這個對象的屬性看做做用域中的標識符,(一樣,也是在運行時)建立一個全新的詞法做用域。

這些機制的缺點是,它壓制了引擎在做用域查詢上進行編譯期優化的能力,由於引擎不得不悲觀地假定這樣的優化是無效的。這兩種特性的結果就是代碼將會運行的更慢。不要使用它們。

附錄:關於eval的一些問題

原文:www.nczonline.net/blog/2013/0…

eval()這個簡單的函數被設計用來執行一個字符串做爲JavaScript代碼,有幾點須要瞭解:

濫用

濫用與性能或安全無關,而是與不理解如何構建和使用JavaScript中的引用有關。假設您有多個表單輸入,其名稱包含一個數字,例如「option1」和「option2」,一般會看到:

function isChecked(optionNumber) {
  return eval("forms[0].option" + optionNumber + ".checked");
}
var result = isChecked(1);
複製代碼

在這種狀況下,開發人員正在嘗試編寫,forms[0].option1.checked但沒有意識到如何在不使用的狀況下作到這一點eval()。你會看到這種類型的模式在大約十歲以上的代碼中不少,由於當時的開發人員不明白如何正確使用該語言。在eval()這裏使用不合適,由於它不是沒必要要的,不是由於它很差。您能夠輕鬆地將此功能重寫爲:

function isChecked(optionNumber) {
  return forms[0]["option" + optionNumber].checked;
}
var result = isChecked(1);
複製代碼

可調試

eval()不容易調試。用 chromeDev 等調試工具沒法打斷點調試,這意味着你將代碼運行到一個黑盒子中,而後從中取出。Chrome開發者工具如今能夠調試 eval() 編碼,但仍然很痛苦。您必須等待代碼執行一次後,纔會顯示在「來源」面板中。避免 eval() 編輯代碼使調試變得更加容易,使您能夠輕鬆查看和逐步瀏覽代碼。

性能

上面有提到,使用時性能確實是一個大問題。

安全

若是你正在接受用戶輸入並eval()以某種方式傳遞它,那麼你是在尋求麻煩。永遠不要這樣作。可是,若是您使用的eval()輸入只有您本身控制而且不能被用戶修改,那麼就沒有安全風險。

因此,只要你的信息源不安全,你的代碼就不安全。不僅僅是由於eval引發的。

相關文章
相關標籤/搜索