JS中有一些操作可以動態地執行JS代碼,並修改或新建詞法作用域,這種操作雖然帶來了更多的靈活性,但是會嚴重地影響到性能。 ...
相關可行的操作
eval
: 同步執行,當前作用域;setTimeout
: 非同步執行,全局作用域;
第1個參數可以傳入函數對象,也可以傳入字元串,即要執行的代碼。
script
: 同步執行,全局作用域;
創建script標簽,並設置
innerHTML
為要執行的代碼。
Function
: 同步執行,全局作用域。
Function構造函數可以傳入字元串,生成一個函數對象。
對詞法作用域的影響
eval
eval
可以通過動態地執行JS代碼從而修改(欺騙)當前的詞法作用域,觀察如下代碼:
function foo(str, a){
eval(str);
console.log(a, b);
}
var b = 2;
foo("var b=3;", 1); // 1, 3
在函數中,執行eval(..)
將var b=3;
帶入該詞法作用域,導致console.log(..)
中對b
的右值引用找到的是3
,而不會查詢到外部的b=2
。
在預設情況下,eval
會對所處的詞法作用域進行修改。
在嚴格模式下,eval
在運行時有其自己的詞法作用域,即其動態執行的JS聲明語句不會影響到eval
語句所處的詞法作用域。
function foo(str){
"use strict";
eval(str);
console.log(a); // ReferenceError: a is not defined
}
foo("var a = 2;");
with
除了eval
另外可以修改詞法作用域的語法是with
關鍵字。
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
接受一個對象,並將這個對象處理為一個完全隔離的詞法作用域,這個對象的屬性會被處理為定義在這個作用域內的詞法標識符。
所以當o1
傳遞給with
時,with
聲明的作用域是o1
,包含了同o1.a
對應的標識符a
,這個左值引用可以找到目標,併成功完成賦值操作。
當o2
傳遞給with
時,with
聲明的詞法作用域會包含同o2.b
對應的標識符b
,但是沒有標識符a
,此時賦值操作會進行LHS標識符查詢,向外層作用域查找。
由於在o2
的作用域、foo
的函數作用域、全局作用域都沒有找到標識符a
,因此當a = 2
執行時,會在全局作用域自動創建一個全局變數(如果是嚴格模式則不會)。
性能問題
JS引擎會在編譯階段進行性能優化,其中部分優化依賴於根據代碼的詞法進行靜態分析,並預先確定所有變數和函數的定義位置,才能在執行過程中快速找到標識符。
eval
和with
這種可能動態更改詞法作用域的操作會導致JS引擎無法在詞法分析階段明確標識符的位置,因此所有優化可能都是無意義的,甚至JS引擎可能在讀取代碼中使用了eval
和with
,就放棄優化了。
因此,在開發中應該避免使用eval
和with
。
引用
[1] Scope and Closures, Kyle Simpson著(O'Reilly, 2014) 978-1-491-33558-8。