細讀《你不知道的JavaScript·上卷》1-2 詞法做用域

墨言妹帶你細讀《你不知道的 JavaScript 》系列的世界,深刻 JavaScript 語言內部,弄清楚 JavaScript 每個零部件的用途,知其然更要知其因此然。html

導讀

在第 1 章中,學習了 做用域,它是一套規則,用來管理引擎如何在當前做用域以及嵌套的子做用域中根據標識符名稱進行變量查找。git

做用域共有兩種主要的工做模型,一是 詞法做用域JavaScript 等);二是 動態做用域bash 腳本等)。github

詞法做用域

  • 什麼是詞法做用域
  • 爲何廢棄欺騙詞法做用域的兩種機制

2.1 詞法階段

在第1章學習了,大部分標準語言編譯器的 第一個工做階段 是詞法化( token 化 )。性能優化

詞法做用域 就是在詞法分析時定義的做用域,即在寫代碼時,由變量和塊做用域的位置決定的。所以,在詞法分析時也是固定不變的了(不考慮欺騙詞法做用域狀況)。bash

下面這段示例代碼有三個嵌套做用域:函數

  • 圈1 包含了全局做用域,只有一個標識符號 foo
  • 圈2 包含 foo 做用域,有三個標識符 abarb
  • 圈3 包含 bar 做用域 ,有一個標識符 c

做用域的範圍 是根據做用域代碼塊定義的位置決定的,在這裏每一個函數建立了一個做用域。post

這裏做用域嵌套是嚴格的,一個函數不能同時存在於兩個外部函數中。 性能

2.1.1 查找

  • 做用域查找會在找到第一個匹配的標識符時中止。學習

  • 遮蔽效應:在多層嵌套做用域中能夠定義同名的標識符,內部的標識符會 遮蔽 外部的標識符。優化

  • 全局變量是全局對象的屬性,被覆蓋的非全局對象則沒法被訪問到。

    window.a
    複製代碼
  • 詞法做用域查找只會查找一級標識符, 好比 abc 。若是代碼中引用了 foo.bar.baz ,詞法做用域只會查找 foo 標識符,找到後,對象屬性訪問規則 會分別接管對 barbaz 屬性的訪問。

2.2 欺騙詞法

欺騙詞法做用域 會致使性能降低,如下兩種方法都 不推薦使用

2.2.1 eval

  • eval(...) 函數能夠接受一個字符串做爲參數,並把字符串的內容看成代碼運行,從而實現對詞法做用域環境的修改。
  • 在執行 eval() 以後的那些代碼,引擎不知道、也不去關心前面的代碼是 動態編譯 的,且 修改 了詞法做用域環境。引擎只會一如既往地進行詞法做用域查找。
非嚴格模式下:
function foo(a, str){
        console.log(str);           //2       // var b = 3;
	eval(str); // 欺騙!
	console.log(eval(str));     //2      //undefined
	console.log(a, b);          //0   2  // 1  3
	console.log(a, window.b);   //0   2  //1  2
}
var b = 2;
foo(0, b);
foo(1, " var b = 3 ;"); 
複製代碼
  • eval() 被調用時,字符串參數 「 var b = 3; 」 被看成真正的代碼聲明瞭變量 b ,並修改了 foo() 的詞法做用域。在 foo() 內部建立了一個變量 b , 遮蔽了外部全局做用域中的同名變量 b
  • console.log() 被執行時,會在 foo() 的內部同時找到 ab , 可是永遠也沒法找到外部的 b 。所以會輸出 1 , 3 ,而不是正常狀況下會輸出的 1 ,2

拓展 eval() 函數,理解值爲 undefiend 的知識,請點擊以下:

MDN 解析 eval() 案例

js中的eval方法詳解(一)

eval()函數的使用

嚴格模式下:
function foo(a, str){
    "use strict";
     console.log(str);           //2       // var b = 3;
     eval(str); 
     console.log(eval(str));    //2       //undefined
     console.log(a, b);         //0  2    // 1  2
     console.log(a, window.b);  //0  2    //1  2
}
var b = 2;
foo(0, b);
foo(1, " var b = 3 ;");
複製代碼
  • eval() 在嚴格模式下,有本身的詞法做用域,其中的聲明沒法修改做用域。
  • setTimeout(...)setInterval(...) 的第一個參數能夠是字符串,字符串的內容會被解釋爲一段動態生成的函數代碼。已廢棄使用
  • 構造函數new Function()的最後一個參數能夠接受代碼字符串(前面的參數是新生成的函數的形參), 避免使用

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 : 4
};

console.log(o1.a);//3
foo(o1);
console.log(o1.a);//2

foo(o2);
console.log(02.a);//undefined
console.log(a);//2 -> 很差,a 被泄露到全局做用域上了!
複製代碼
  • o1 傳進後,with 聲明的做用域是 o1a = 2 賦值操做找到 o1.a 並將 2 賦值給它。
  • o2 傳進後,做用域 o2 中沒有 a 屬性,則進行 LHS 標識符查找,o2 的做用域、 foo() 的做用域 和全局做用域都沒找到標識符 a ,所以當 a = 2 執行時,產生反作用,自動建立了一個全局變量(非嚴格模式)a ,並將 2 賦值給 a ,因此 o2.a 保持 undefined

在嚴格模式下,with 語句被徹底禁用,eval() 則只保留核心功能,都不推薦使用。

2.2.3 性能

JavaScript 引擎在 編譯階段 進行各類性能優化,一些優化在詞法分析階段,靜態分析了代碼,預先肯定了變量和函數聲明的位置,因此在執行期間就能夠快速解析標識符。

2.3小結

詞法做用域只由函數被聲明時所處的位置決定。

如下兩個機制能夠 欺騙 詞法做用域:

  • eval(...) : 對一段包含一個或多個聲明的 代碼 字符串進行演算,藉此來修改已經存在的詞法做用域(運行時)。
  • with : 將一個對象的引用 看成 做用域,將對象的屬性看成做用域的標識符,建立一個新的詞法做用域(運行時)。

反作用 是引擎沒法在編譯時對做用域查找進行優化。由於引擎只能謹慎地認爲這樣的優化是無效的,使用任何一個機制都將致使代碼運行變慢。廢棄它們。

最後, 讀書是由厚到薄,又由薄到厚的雙向過程,注重領悟、實踐,不斷踩坑、提高,如有幫助,請點個贊,謝謝您的支持與指教。

參考文獻:

木易楊博客

隙游塵博客

taopoppy 博客

歷史文章:

細讀《你不知道的JavaScript·上卷》1-1 做用域是什麼

【譯】30 Seconds of ES6 (一)

相關文章
相關標籤/搜索