1. 程式人生 > >深入學習js之淺談作用域之(eval()和with)

深入學習js之淺談作用域之(eval()和with)

在深入學習js之淺談作用域(一)中

將作用域定義為一套規則,用來管理引擎如何在當前作用域以及巢狀的子作用域中根據識別符號名稱進行變數查詢。

作用域分為兩種主要的工作模式:1.詞法作用域(大多數程式語言包括js)

2.動態作用域(Bash指令碼、Perl中的一些模式)

1.巢狀詞法作用域的栗子和“遮蔽效應”

以下巢狀作用域的栗子中

1.
		function foo(a) {
			2.
			var b = a * 2;
			function bar(c) {
				3.
				console.log(a, b, c);
			}

			bar(b * 3);
		}
		foo(2);

1.包含著整個全域性作用域,其中只有一個識別符號:foo

2.包含著foo建立的作用域,其中有三個識別符號:a、bar和b

3.包含著bar所建立的作用域,其中只有一個識別符號:c

遮蔽效應

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

”遮蔽效應“的實質是作用域查詢始終從執行時所處的最內部作用域開始,逐級向外或者向上進行,直到遇見第一個匹配的識別符號為止

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

來對其進行訪問

window.a

var a = 1;
		function foo() {
			var a = 2;
			console.log(a);//2
			function bar() {
				var a = 3;
				console.log(a);//3
				console.log(window.a);//1
			}
			bar();
		}
		console.log(a);//1

		foo();


通過這種技術可以訪問那些被同名變數所遮蔽的全域性變數。但非全域性的變數如果被遮蔽了,無論如何都無法被訪問到。

無論函式在哪裡被呼叫,也無論它如何被呼叫,它的詞法作用域都只由函式被宣告時所處的位置決定。

詞法作用域查詢只會查詢一級識別符號,比如window。如果程式碼中引用window.document詞法作用域查詢只會試圖查詢window識別符號,找到這個變數後,物件的屬性訪問規則接管對document屬性的訪問。

2.欺騙語法以及eval和with的禁用緣由

如果詞法作用域完全由寫程式碼期間函式所宣告的位置來定義,那麼怎樣才能在執行時來”修改(欺騙)“作用域呢?

eval和with。

2.1eval 

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

eval(..)呼叫的"var b = 3;"這段程式碼會被當作本來就在那裡一樣來處理。由於那段程式碼聲明瞭一個新的變數b,因此它對已經存在的foo()的詞法作用域進行了修改。

事實上,和前面提到的原理一樣,這段程式碼實際上在foo()內部建立了一個變數b並且遮蔽了外部作用域中的同名變數。

當console.log(..)被執行時,會在foo(..)的內部同時找到a和b,但是永遠也無法找到外部的b。

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

function foo(str) {
			"use strict"
			eval(str);
			console.log(a);//undefined
		}
		foo("var a = 3;");
另外eval(..)中程式碼的執行也不安全。不建議使用

2.2with()
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;
		}
執行完,但實際上這不僅是為了方便地訪問物件屬性


這個例子中串講了o1和o2兩個物件,其中一個具有a屬性,另外一個沒有。foo()函式接收一個obj引數,該引數是一個物件引用,並對這個物件引用執行了with(obj){...}.在with塊內部,我們寫的程式碼看起來只是對變數a進行簡單的詞法引用,實際上就是一個LHS引用,並將2賦值給它。

當o1傳進去的時候,a=2賦值操作找到了o1.a並將2賦值給它

當o2傳進去的時候,沒有找到o2的a屬性,因此不會建立這個屬性,o2.a保持undefined

那麼a = 2賦值操作建立了一個全域性變數a是什麼鬼?

with可以將一個沒有或有多個屬性的物件處理為一個完全隔絕的詞法作用域,因此這個物件的屬性也會被處理為定義在這個作用域中的詞法識別符號

當我們傳遞o1給with時,with所宣告的作用域是o1,而這個作用域中含有一個同o1.a屬性相符的識別符號。但當我們將o2作為作用域時,其中並沒有a識別符號

因此進行了LHS識別符號查詢,o2的作用域、foo的作用域,全域性作用域都沒有找到識別符號a,因此當a=2執行時,自動建立了一個全域性變數(非嚴格模式)

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

eval函式如果接受了含有一個或多個宣告的程式碼,就會修改其所處的詞法作用域,而with宣告實際上是根據你傳遞給他的物件憑空建立了一個全新的詞法作用域

其次js引擎會在編譯階段進行效能優化,其中有些效能優化能夠根據程式碼的詞法進行靜態分析,並預設確定所有變數和函式的定義位置,才能在執行過程中快速的找到識別符號

,但如果引擎在程式碼中發現了eval()和with,它只能假設關於識別符號位置的判斷是無效的,會影響優化。

總結禁用eval和with的原因

1.會被嚴格模式影響(eval在保留核心功能、安全情況下可使用,with被完全禁止)

2會導致效能下降

最好不要使用它們